summaryrefslogtreecommitdiffstats
path: root/configserver
diff options
context:
space:
mode:
authorHarald Musum <musum@verizonmedia.com>2020-09-07 14:24:26 +0200
committerHarald Musum <musum@verizonmedia.com>2020-09-07 14:24:26 +0200
commitd29de4a867577220a6afb838175fe73c39f1bccd (patch)
tree8275984b86f6799c6614df8a4c4ecd67ec66c5e3 /configserver
parentdb9105023dc06c7ca56a2914735dba663bd21d5c (diff)
Simplify and move in the direction of unifying remote and local sessions
Move tests and avoid a lot of low-level setup code Do not use `synchronized` for ensureApplicationLoaded()
Diffstat (limited to 'configserver')
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java9
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java.orig1100
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java7
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java100
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java2
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionCache.java2
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java115
-rw-r--r--configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java4
-rw-r--r--configserver/src/test/apps/app-major-version-2/deployment.xml1
-rw-r--r--configserver/src/test/apps/app-major-version-2/hosts.xml7
-rw-r--r--configserver/src/test/apps/app-major-version-2/searchdefinitions/music.sd50
-rw-r--r--configserver/src/test/apps/app-major-version-2/services.xml38
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java2
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcServerTest.java3
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java299
-rw-r--r--configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java175
16 files changed, 1501 insertions, 413 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
index eb13baf3e6b..a4a705c747e 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java
@@ -610,9 +610,10 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
try {
Tenant tenant = getTenant(applicationId);
if (tenant == null) throw new NotFoundException("Tenant '" + applicationId.tenant() + "' not found");
- long sessionId = getSessionIdForApplication(tenant, applicationId);
- RemoteSession session = getRemoteSession(tenant, sessionId);
- return session.ensureApplicationLoaded().getForVersionOrLatest(version, clock.instant());
+ RemoteSession session = getActiveSession(applicationId);
+ if (session == null) throw new NotFoundException("No active session found for '" + applicationId + "'");
+ SessionRepository sessionRepository = tenant.getSessionRepository();
+ return sessionRepository.ensureApplicationLoaded(session).getForVersionOrLatest(version, clock.instant());
} catch (NotFoundException e) {
log.log(Level.WARNING, "Failed getting application for '" + applicationId + "': " + e.getMessage());
throw e;
@@ -951,7 +952,7 @@ public class ApplicationRepository implements com.yahoo.config.provision.Deploye
try {
long currentActiveSessionId = applicationRepo.requireActiveSessionOf(appId);
RemoteSession currentActiveSession = getRemoteSession(tenant, currentActiveSessionId);
- currentActiveApplicationSet = Optional.ofNullable(currentActiveSession.ensureApplicationLoaded());
+ currentActiveApplicationSet = Optional.ofNullable(tenant.getSessionRepository().ensureApplicationLoaded(currentActiveSession));
} catch (IllegalArgumentException e) {
// Do nothing if we have no currently active session
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java.orig b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java.orig
new file mode 100644
index 00000000000..f1ddc76e632
--- /dev/null
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationRepository.java.orig
@@ -0,0 +1,1100 @@
+// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.vespa.config.server;
+
+import com.google.inject.Inject;
+import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.component.Version;
+import com.yahoo.config.FileReference;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.model.api.HostInfo;
+import com.yahoo.config.model.api.ServiceInfo;
+import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.Environment;
+import com.yahoo.config.provision.HostFilter;
+import com.yahoo.config.provision.InfraDeployer;
+import com.yahoo.config.provision.Provisioner;
+import com.yahoo.config.provision.RegionName;
+import com.yahoo.config.provision.SystemName;
+import com.yahoo.config.provision.TenantName;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.container.jdisc.HttpResponse;
+import com.yahoo.docproc.jdisc.metric.NullMetric;
+import com.yahoo.io.IOUtils;
+import com.yahoo.jdisc.Metric;
+import com.yahoo.path.Path;
+import com.yahoo.slime.Slime;
+import com.yahoo.transaction.NestedTransaction;
+import com.yahoo.transaction.Transaction;
+import com.yahoo.vespa.config.server.application.Application;
+import com.yahoo.vespa.config.server.application.ApplicationSet;
+import com.yahoo.vespa.config.server.application.CompressedApplicationInputStream;
+import com.yahoo.vespa.config.server.application.ConfigConvergenceChecker;
+import com.yahoo.vespa.config.server.application.FileDistributionStatus;
+import com.yahoo.vespa.config.server.application.HttpProxy;
+import com.yahoo.vespa.config.server.application.TenantApplications;
+import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
+import com.yahoo.vespa.config.server.configchange.RefeedActions;
+import com.yahoo.vespa.config.server.configchange.RestartActions;
+import com.yahoo.vespa.config.server.deploy.DeployHandlerLogger;
+import com.yahoo.vespa.config.server.deploy.Deployment;
+import com.yahoo.vespa.config.server.deploy.InfraDeployerProvider;
+import com.yahoo.vespa.config.server.http.InternalServerException;
+import com.yahoo.vespa.config.server.http.LogRetriever;
+import com.yahoo.vespa.config.server.http.SimpleHttpFetcher;
+import com.yahoo.vespa.config.server.http.TesterClient;
+import com.yahoo.vespa.config.server.http.v2.DeploymentMetricsResponse;
+import com.yahoo.vespa.config.server.http.v2.PrepareResult;
+import com.yahoo.vespa.config.server.http.v2.ProtonMetricsResponse;
+import com.yahoo.vespa.config.server.metrics.DeploymentMetricsRetriever;
+import com.yahoo.vespa.config.server.metrics.ProtonMetricsRetriever;
+import com.yahoo.vespa.config.server.provision.HostProvisionerProvider;
+import com.yahoo.vespa.config.server.session.LocalSession;
+import com.yahoo.vespa.config.server.session.PrepareParams;
+import com.yahoo.vespa.config.server.session.RemoteSession;
+import com.yahoo.vespa.config.server.session.Session;
+import com.yahoo.vespa.config.server.session.SessionRepository;
+import com.yahoo.vespa.config.server.session.SilentDeployLogger;
+import com.yahoo.vespa.config.server.tenant.ApplicationRolesStore;
+import com.yahoo.vespa.config.server.tenant.ContainerEndpointsCache;
+import com.yahoo.vespa.config.server.tenant.EndpointCertificateMetadataStore;
+import com.yahoo.vespa.config.server.tenant.Tenant;
+import com.yahoo.vespa.config.server.tenant.TenantMetaData;
+import com.yahoo.vespa.config.server.tenant.TenantRepository;
+import com.yahoo.vespa.curator.Curator;
+import com.yahoo.vespa.curator.Lock;
+import com.yahoo.vespa.defaults.Defaults;
+import com.yahoo.vespa.flags.FlagSource;
+import com.yahoo.vespa.flags.InMemoryFlagSource;
+import com.yahoo.vespa.orchestrator.Orchestrator;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static com.yahoo.config.model.api.container.ContainerServiceType.CLUSTERCONTROLLER_CONTAINER;
+import static com.yahoo.config.model.api.container.ContainerServiceType.CONTAINER;
+import static com.yahoo.config.model.api.container.ContainerServiceType.LOGSERVER_CONTAINER;
+import static com.yahoo.vespa.config.server.filedistribution.FileDistributionUtil.fileReferenceExistsOnDisk;
+import static com.yahoo.vespa.config.server.filedistribution.FileDistributionUtil.getFileReferencesOnDisk;
+import static com.yahoo.vespa.config.server.tenant.TenantRepository.HOSTED_VESPA_TENANT;
+import static com.yahoo.vespa.curator.Curator.CompletionWaiter;
+import static com.yahoo.yolean.Exceptions.uncheck;
+import static java.nio.file.Files.readAttributes;
+
+/**
+ * The API for managing applications.
+ *
+ * @author bratseth
+ */
+// TODO: Move logic for dealing with applications here from the HTTP layer and make this the persistent component
+// owning the rest of the state
+public class ApplicationRepository implements com.yahoo.config.provision.Deployer {
+
+ private static final Logger log = Logger.getLogger(ApplicationRepository.class.getName());
+
+ private final TenantRepository tenantRepository;
+ private final Optional<Provisioner> hostProvisioner;
+ private final Optional<InfraDeployer> infraDeployer;
+ private final ConfigConvergenceChecker convergeChecker;
+ private final HttpProxy httpProxy;
+ private final Clock clock;
+ private final DeployLogger logger = new SilentDeployLogger();
+ private final ConfigserverConfig configserverConfig;
+ private final FileDistributionStatus fileDistributionStatus = new FileDistributionStatus();
+ private final Orchestrator orchestrator;
+ private final LogRetriever logRetriever;
+ private final TesterClient testerClient;
+ private final Metric metric;
+
+ @Inject
+ public ApplicationRepository(TenantRepository tenantRepository,
+ HostProvisionerProvider hostProvisionerProvider,
+ InfraDeployerProvider infraDeployerProvider,
+ ConfigConvergenceChecker configConvergenceChecker,
+ HttpProxy httpProxy,
+ ConfigserverConfig configserverConfig,
+ Orchestrator orchestrator,
+ TesterClient testerClient,
+ Metric metric,
+ FlagSource flagSource) {
+ this(tenantRepository,
+ hostProvisionerProvider.getHostProvisioner(),
+ infraDeployerProvider.getInfraDeployer(),
+ configConvergenceChecker,
+ httpProxy,
+ configserverConfig,
+ orchestrator,
+ new LogRetriever(),
+ Clock.systemUTC(),
+ testerClient,
+ metric,
+ flagSource);
+ }
+
+ private ApplicationRepository(TenantRepository tenantRepository,
+ Optional<Provisioner> hostProvisioner,
+ Optional<InfraDeployer> infraDeployer,
+ ConfigConvergenceChecker configConvergenceChecker,
+ HttpProxy httpProxy,
+ ConfigserverConfig configserverConfig,
+ Orchestrator orchestrator,
+ LogRetriever logRetriever,
+ Clock clock,
+ TesterClient testerClient,
+ Metric metric,
+ FlagSource flagSource) {
+ this.tenantRepository = Objects.requireNonNull(tenantRepository);
+ this.hostProvisioner = Objects.requireNonNull(hostProvisioner);
+ this.infraDeployer = Objects.requireNonNull(infraDeployer);
+ this.convergeChecker = Objects.requireNonNull(configConvergenceChecker);
+ this.httpProxy = Objects.requireNonNull(httpProxy);
+ this.configserverConfig = Objects.requireNonNull(configserverConfig);
+ this.orchestrator = Objects.requireNonNull(orchestrator);
+ this.logRetriever = Objects.requireNonNull(logRetriever);
+ this.clock = Objects.requireNonNull(clock);
+ this.testerClient = Objects.requireNonNull(testerClient);
+ this.metric = Objects.requireNonNull(metric);
+ }
+
+ public static class Builder {
+ private TenantRepository tenantRepository;
+ private Optional<Provisioner> hostProvisioner;
+ private HttpProxy httpProxy = new HttpProxy(new SimpleHttpFetcher());
+ private Clock clock = Clock.systemUTC();
+ private ConfigserverConfig configserverConfig = new ConfigserverConfig.Builder().build();
+ private Orchestrator orchestrator;
+ private LogRetriever logRetriever = new LogRetriever();
+ private TesterClient testerClient = new TesterClient();
+ private Metric metric = new NullMetric();
+ private FlagSource flagSource = new InMemoryFlagSource();
+
+ public Builder withTenantRepository(TenantRepository tenantRepository) {
+ this.tenantRepository = tenantRepository;
+ return this;
+ }
+
+ public Builder withClock(Clock clock) {
+ this.clock = clock;
+ return this;
+ }
+
+ public Builder withProvisioner(Provisioner provisioner) {
+ if (this.hostProvisioner != null) throw new IllegalArgumentException("provisioner already set in builder");
+ this.hostProvisioner = Optional.ofNullable(provisioner);
+ return this;
+ }
+
+ public Builder withHostProvisionerProvider(HostProvisionerProvider hostProvisionerProvider) {
+ if (this.hostProvisioner != null) throw new IllegalArgumentException("provisioner already set in builder");
+ this.hostProvisioner = hostProvisionerProvider.getHostProvisioner();
+ return this;
+ }
+
+ public Builder withHttpProxy(HttpProxy httpProxy) {
+ this.httpProxy = httpProxy;
+ return this;
+ }
+
+ public Builder withConfigserverConfig(ConfigserverConfig configserverConfig) {
+ this.configserverConfig = configserverConfig;
+ return this;
+ }
+
+ public Builder withOrchestrator(Orchestrator orchestrator) {
+ this.orchestrator = orchestrator;
+ return this;
+ }
+
+ public Builder withLogRetriever(LogRetriever logRetriever) {
+ this.logRetriever = logRetriever;
+ return this;
+ }
+
+ public Builder withTesterClient(TesterClient testerClient) {
+ this.testerClient = testerClient;
+ return this;
+ }
+
+ public Builder withFlagSource(FlagSource flagSource) {
+ this.flagSource = flagSource;
+ return this;
+ }
+
+ public Builder withMetric(Metric metric) {
+ this.metric = metric;
+ return this;
+ }
+
+ public ApplicationRepository build() {
+ return new ApplicationRepository(tenantRepository,
+ hostProvisioner,
+ InfraDeployerProvider.empty().getInfraDeployer(),
+ new ConfigConvergenceChecker(),
+ httpProxy,
+ configserverConfig,
+ orchestrator,
+ logRetriever,
+ clock,
+ testerClient,
+ metric,
+ flagSource);
+ }
+
+ }
+
+ public Metric metric() {
+ return metric;
+ }
+
+ // ---------------- Deploying ----------------------------------------------------------------
+
+ public PrepareResult prepare(Tenant tenant, long sessionId, PrepareParams prepareParams, Instant now) {
+ validateThatLocalSessionIsNotActive(tenant, sessionId);
+ LocalSession session = getLocalSession(tenant, sessionId);
+ ApplicationId applicationId = prepareParams.getApplicationId();
+ Optional<ApplicationSet> currentActiveApplicationSet = getCurrentActiveApplicationSet(tenant, applicationId);
+ Slime deployLog = createDeployLog();
+ DeployLogger logger = new DeployHandlerLogger(deployLog.get().setArray("log"), prepareParams.isVerbose(), applicationId);
+ try (ActionTimer timer = timerFor(applicationId, "deployment.prepareMillis")) {
+ SessionRepository sessionRepository = tenant.getSessionRepository();
+ ConfigChangeActions actions = sessionRepository.prepareLocalSession(session, logger, prepareParams,
+ currentActiveApplicationSet, tenant.getPath(), now);
+ logConfigChangeActions(actions, logger);
+ log.log(Level.INFO, TenantRepository.logPre(applicationId) + "Session " + sessionId + " prepared successfully. ");
+ return new PrepareResult(sessionId, actions, deployLog);
+ }
+ }
+
+ public PrepareResult deploy(CompressedApplicationInputStream in, PrepareParams prepareParams,
+ boolean ignoreSessionStaleFailure, Instant now) {
+ File tempDir = uncheck(() -> Files.createTempDirectory("deploy")).toFile();
+ PrepareResult prepareResult;
+ try {
+ prepareResult = deploy(decompressApplication(in, tempDir), prepareParams, ignoreSessionStaleFailure, now);
+ } finally {
+ cleanupTempDirectory(tempDir);
+ }
+ return prepareResult;
+ }
+
+ public PrepareResult deploy(File applicationPackage, PrepareParams prepareParams) {
+ return deploy(applicationPackage, prepareParams, false, Instant.now());
+ }
+
+ public PrepareResult deploy(File applicationPackage, PrepareParams prepareParams,
+ boolean ignoreSessionStaleFailure, Instant now) {
+ ApplicationId applicationId = prepareParams.getApplicationId();
+ long sessionId = createSession(applicationId, prepareParams.getTimeoutBudget(), applicationPackage);
+ Tenant tenant = getTenant(applicationId);
+ PrepareResult result = prepare(tenant, sessionId, prepareParams, now);
+ activate(tenant, sessionId, prepareParams.getTimeoutBudget(), ignoreSessionStaleFailure);
+ return result;
+ }
+
+ /**
+ * Creates a new deployment from the active application, if available.
+ * This is used for system internal redeployments, not on application package changes.
+ *
+ * @param application the active application to be redeployed
+ * @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) {
+ return deployFromLocalActive(application, false);
+ }
+
+ /**
+ * Creates a new deployment from the active application, if available.
+ * This is used for system internal redeployments, not on application package changes.
+ *
+ * @param application the active application to be redeployed
+ * @param bootstrap the deployment is done when bootstrapping
+ * @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,
+ boolean bootstrap) {
+ return deployFromLocalActive(application,
+ Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout()).plus(Duration.ofSeconds(5)),
+ bootstrap);
+ }
+
+ /**
+ * Creates a new deployment from the active application, if available.
+ * This is used for system internal redeployments, not on application package changes.
+ *
+ * @param application the active application to be redeployed
+ * @param timeout the timeout to use for each individual deployment operation
+ * @param bootstrap the deployment is done when bootstrapping
+ * @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,
+ boolean bootstrap) {
+ Optional<com.yahoo.config.provision.Deployment> infraDeployment = infraDeployer.flatMap(d -> d.getDeployment(application));
+ if (infraDeployment.isPresent()) return infraDeployment;
+
+ Tenant tenant = tenantRepository.getTenant(application.tenant());
+ if (tenant == null) return Optional.empty();
+ LocalSession activeSession = getActiveLocalSession(tenant, application);
+ if (activeSession == null) return Optional.empty();
+ TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout);
+ SessionRepository sessionRepository = tenant.getSessionRepository();
+ LocalSession newSession = sessionRepository.createSessionFromExisting(activeSession, logger, true, timeoutBudget);
+ sessionRepository.addLocalSession(newSession);
+
+ return Optional.of(Deployment.unprepared(newSession, this, hostProvisioner, tenant, timeout, clock,
+ false /* don't validate as this is already deployed */, bootstrap));
+ }
+
+ @Override
+ public Optional<Instant> lastDeployTime(ApplicationId application) {
+ Tenant tenant = tenantRepository.getTenant(application.tenant());
+ if (tenant == null) return Optional.empty();
+ RemoteSession activeSession = getActiveSession(tenant, application);
+ if (activeSession == null) return Optional.empty();
+ return Optional.of(activeSession.getCreateTime());
+ }
+
+ public ApplicationId activate(Tenant tenant,
+ long sessionId,
+ TimeoutBudget timeoutBudget,
+ boolean ignoreSessionStaleFailure) {
+ LocalSession localSession = getLocalSession(tenant, sessionId);
+ Deployment deployment = deployFromPreparedSession(localSession, tenant, timeoutBudget.timeLeft());
+ deployment.setIgnoreSessionStaleFailure(ignoreSessionStaleFailure);
+ deployment.activate();
+ return localSession.getApplicationId();
+ }
+
+ private Deployment deployFromPreparedSession(LocalSession session, Tenant tenant, Duration timeout) {
+ return Deployment.prepared(session, this, hostProvisioner, tenant, timeout, clock, false);
+ }
+
+ public Transaction deactivateCurrentActivateNew(Session active, LocalSession prepared, boolean ignoreStaleSessionFailure) {
+ Tenant tenant = tenantRepository.getTenant(prepared.getTenantName());
+ Transaction transaction = tenant.getSessionRepository().createActivateTransaction(prepared);
+ if (active != null) {
+ checkIfActiveHasChanged(prepared, active, ignoreStaleSessionFailure);
+ checkIfActiveIsNewerThanSessionToBeActivated(prepared.getSessionId(), active.getSessionId());
+ transaction.add(active.createDeactivateTransaction().operations());
+ }
+ transaction.add(updateMetaDataWithDeployTimestamp(tenant, clock.instant()));
+ return transaction;
+ }
+
+ private List<Transaction.Operation> updateMetaDataWithDeployTimestamp(Tenant tenant, Instant deployTimestamp) {
+ TenantMetaData tenantMetaData = getTenantMetaData(tenant).withLastDeployTimestamp(deployTimestamp);
+ return tenantRepository.createWriteTenantMetaDataTransaction(tenantMetaData).operations();
+ }
+
+ TenantMetaData getTenantMetaData(Tenant tenant) {
+ return tenantRepository.getTenantMetaData(tenant);
+ }
+
+ static void checkIfActiveHasChanged(LocalSession session, Session currentActiveSession, boolean ignoreStaleSessionFailure) {
+ long activeSessionAtCreate = session.getActiveSessionAtCreate();
+ log.log(Level.FINE, 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(Level.FINE, 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 ActivationConflictException(errMsg);
+ }
+ }
+ }
+
+ // As of now, config generation is based on session id, and config generation must be a monotonically
+ // increasing number
+ static void checkIfActiveIsNewerThanSessionToBeActivated(long sessionId, long currentActiveSessionId) {
+ if (sessionId < currentActiveSessionId) {
+ throw new ActivationConflictException("It is not possible to activate session " + sessionId +
+ ", because it is older than current active session (" +
+ currentActiveSessionId + ")");
+ }
+ }
+
+ // ---------------- Application operations ----------------------------------------------------------------
+
+ /**
+ * Deletes an application
+ *
+ * @return true if the application was found and deleted, false if it was not present
+ * @throws RuntimeException if the delete transaction fails. This method is exception safe.
+ */
+ boolean delete(ApplicationId applicationId) {
+ return delete(applicationId, Duration.ofSeconds(60));
+ }
+
+ /**
+ * Deletes an application
+ *
+ * @return true if the application was found and deleted, false if it was not present
+ * @throws RuntimeException if the delete transaction fails. This method is exception safe.
+ */
+ public boolean delete(ApplicationId applicationId, Duration waitTime) {
+ Tenant tenant = getTenant(applicationId);
+ if (tenant == null) return false;
+
+ TenantApplications tenantApplications = tenant.getApplicationRepo();
+ try (Lock lock = tenantApplications.lock(applicationId)) {
+ if ( ! tenantApplications.exists(applicationId)) return false;
+
+ Optional<Long> activeSession = tenantApplications.activeSessionOf(applicationId);
+ if (activeSession.isEmpty()) return false;
+
+ // Deleting an application is done by deleting the remote session and waiting
+ // until the config server where the deployment happened picks it up and deletes
+ // the local session
+ long sessionId = activeSession.get();
+
+ RemoteSession remoteSession;
+ try {
+ remoteSession = getRemoteSession(tenant, sessionId);
+ Transaction deleteTransaction = remoteSession.createDeleteTransaction();
+ deleteTransaction.commit();
+ log.log(Level.INFO, TenantRepository.logPre(applicationId) + "Waiting for session " + sessionId + " to be deleted");
+
+ if ( ! waitTime.isZero() && localSessionHasBeenDeleted(applicationId, sessionId, waitTime)) {
+ log.log(Level.INFO, TenantRepository.logPre(applicationId) + "Session " + sessionId + " deleted");
+ } else {
+ deleteTransaction.rollbackOrLog();
+ throw new InternalServerException(applicationId + " was not deleted (waited " + waitTime + "), session " + sessionId);
+ }
+ } catch (NotFoundException e) {
+ // For the case where waiting timed out in a previous attempt at deleting the application, continue and do the steps below
+ log.log(Level.INFO, TenantRepository.logPre(applicationId) + "Active session exists, but has not been deleted properly. Trying to cleanup");
+ }
+
+ NestedTransaction transaction = new NestedTransaction();
+ Curator curator = tenantRepository.getCurator();
+ transaction.add(new ContainerEndpointsCache(tenant.getPath(), curator).delete(applicationId)); // TODO: Not unit tested
+ // Delete any application roles
+ transaction.add(new ApplicationRolesStore(curator, tenant.getPath()).delete(applicationId));
+ // Delete endpoint certificates
+ transaction.add(new EndpointCertificateMetadataStore(curator, tenant.getPath()).delete(applicationId));
+ // (When rotations are updated in zk, we need to redeploy the zone app, on the right config server
+ // this is done asynchronously in application maintenance by the node repository)
+ transaction.add(tenantApplications.createDeleteTransaction(applicationId));
+
+ hostProvisioner.ifPresent(provisioner -> provisioner.remove(transaction, applicationId));
+ transaction.onCommitted(() -> log.log(Level.INFO, "Deleted " + applicationId));
+ transaction.commit();
+ return true;
+ }
+ }
+
+ public HttpResponse clusterControllerStatusPage(ApplicationId applicationId, String hostName, String pathSuffix) {
+ // WARNING: pathSuffix may be given by the external user. Make sure no security issues arise...
+ // We should be OK here, because at most, pathSuffix may change the parent path, but cannot otherwise
+ // change the hostname and port. Exposing other paths on the cluster controller should be fine.
+ // TODO: It would be nice to have a simple check to verify pathSuffix doesn't contain /../ components.
+ String relativePath = "clustercontroller-status/" + pathSuffix;
+
+ return httpProxy.get(getApplication(applicationId), hostName,
+ CLUSTERCONTROLLER_CONTAINER.serviceName, relativePath);
+ }
+
+ public Long getApplicationGeneration(ApplicationId applicationId) {
+ return getApplication(applicationId).getApplicationGeneration();
+ }
+
+ public void restart(ApplicationId applicationId, HostFilter hostFilter) {
+ hostProvisioner.ifPresent(provisioner -> provisioner.restart(applicationId, hostFilter));
+ }
+
+ public boolean isSuspended(ApplicationId application) {
+ return orchestrator.getAllSuspendedApplications().contains(application);
+ }
+
+ public HttpResponse filedistributionStatus(ApplicationId applicationId, Duration timeout) {
+ return fileDistributionStatus.status(getApplication(applicationId), timeout);
+ }
+
+ public Set<String> deleteUnusedFiledistributionReferences(File fileReferencesPath, Duration keepFileReferences) {
+ if (!fileReferencesPath.isDirectory()) throw new RuntimeException(fileReferencesPath + " is not a directory");
+
+ Set<String> fileReferencesInUse = new HashSet<>();
+ // Intentionally skip applications that we for some reason do not find
+ // or that we fail to get file references for (they will be retried on the next run)
+ for (var applicationId : listApplications()) {
+ try {
+ Optional<Application> app = getOptionalApplication(applicationId);
+ if (app.isEmpty()) continue;
+ fileReferencesInUse.addAll(app.get().getModel().fileReferences().stream()
+ .map(FileReference::value)
+ .collect(Collectors.toSet()));
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Getting file references in use for '" + applicationId + "' failed", e);
+ }
+ }
+ log.log(Level.FINE, "File references in use : " + fileReferencesInUse);
+
+ // Find those on disk that are not in use
+ Set<String> fileReferencesOnDisk = getFileReferencesOnDisk(fileReferencesPath);
+ log.log(Level.FINE, "File references on disk (in " + fileReferencesPath + "): " + fileReferencesOnDisk);
+
+ Instant instant = Instant.now().minus(keepFileReferences);
+ Set<String> fileReferencesToDelete = fileReferencesOnDisk
+ .stream()
+ .filter(fileReference -> ! fileReferencesInUse.contains(fileReference))
+ .filter(fileReference -> isFileLastModifiedBefore(new File(fileReferencesPath, fileReference), instant))
+ .collect(Collectors.toSet());
+ if (fileReferencesToDelete.size() > 0) {
+ log.log(Level.FINE, "Will delete file references not in use: " + fileReferencesToDelete);
+ fileReferencesToDelete.forEach(fileReference -> {
+ File file = new File(fileReferencesPath, fileReference);
+ if ( ! IOUtils.recursiveDeleteDir(file))
+ log.log(Level.WARNING, "Could not delete " + file.getAbsolutePath());
+ });
+ }
+ return fileReferencesToDelete;
+ }
+
+ public Set<FileReference> getFileReferences(ApplicationId applicationId) {
+ return getOptionalApplication(applicationId).map(app -> app.getModel().fileReferences()).orElse(Set.of());
+ }
+
+ public ApplicationFile getApplicationFileFromSession(TenantName tenantName, long sessionId, String path, LocalSession.Mode mode) {
+ Tenant tenant = tenantRepository.getTenant(tenantName);
+ return getLocalSession(tenant, sessionId).getApplicationFile(Path.fromString(path), mode);
+ }
+
+ public Tenant getTenant(ApplicationId applicationId) {
+ return tenantRepository.getTenant(applicationId.tenant());
+ }
+
+ private Application getApplication(ApplicationId applicationId) {
+ return getApplication(applicationId, Optional.empty());
+ }
+
+ private Application getApplication(ApplicationId applicationId, Optional<Version> version) {
+ try {
+ Tenant tenant = getTenant(applicationId);
+ if (tenant == null) throw new NotFoundException("Tenant '" + applicationId.tenant() + "' not found");
+ RemoteSession session = getActiveSession(applicationId);
+ SessionRepository sessionRepository = tenant.getSessionRepository();
+ return sessionRepository.ensureApplicationLoaded(session).getForVersionOrLatest(version, clock.instant());
+ } catch (NotFoundException e) {
+ log.log(Level.WARNING, "Failed getting application for '" + applicationId + "': " + e.getMessage());
+ throw e;
+ } catch (Exception e) {
+ log.log(Level.WARNING, "Failed getting application for '" + applicationId + "'", e);
+ throw e;
+ }
+ }
+
+ // Will return Optional.empty() if getting application fails (instead of throwing an exception)
+ private Optional<Application> getOptionalApplication(ApplicationId applicationId) {
+ try {
+ return Optional.of(getApplication(applicationId));
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ public Set<ApplicationId> listApplications() {
+ return tenantRepository.getAllTenants().stream()
+ .flatMap(tenant -> tenant.getApplicationRepo().activeApplications().stream())
+ .collect(Collectors.toSet());
+ }
+
+ private boolean isFileLastModifiedBefore(File fileReference, Instant instant) {
+ BasicFileAttributes fileAttributes;
+ try {
+ fileAttributes = readAttributes(fileReference.toPath(), BasicFileAttributes.class);
+ return fileAttributes.lastModifiedTime().toInstant().isBefore(instant);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private boolean localSessionHasBeenDeleted(ApplicationId applicationId, long sessionId, Duration waitTime) {
+ SessionRepository sessionRepository = getTenant(applicationId).getSessionRepository();
+ Instant end = Instant.now().plus(waitTime);
+ do {
+ if (sessionRepository.getRemoteSession(sessionId) == null) return true;
+ try { Thread.sleep(10); } catch (InterruptedException e) { /* ignored */}
+ } while (Instant.now().isBefore(end));
+
+ return false;
+ }
+
+ public Optional<String> getApplicationPackageReference(ApplicationId applicationId) {
+ Optional<String> applicationPackage = Optional.empty();
+ RemoteSession session = getActiveSession(applicationId);
+ if (session != null) {
+ FileReference applicationPackageReference = session.getApplicationPackageReference();
+ File downloadDirectory = new File(Defaults.getDefaults().underVespaHome(configserverConfig().fileReferencesDir()));
+ if (applicationPackageReference != null && ! fileReferenceExistsOnDisk(downloadDirectory, applicationPackageReference))
+ applicationPackage = Optional.of(applicationPackageReference.value());
+ }
+ return applicationPackage;
+ }
+
+ public List<Version> getAllVersions(ApplicationId applicationId) {
+ Optional<ApplicationSet> applicationSet = getCurrentActiveApplicationSet(getTenant(applicationId), applicationId);
+ if (applicationSet.isEmpty())
+ return List.of();
+ else
+ return applicationSet.get().getAllVersions(applicationId);
+ }
+
+ // ---------------- Convergence ----------------------------------------------------------------
+
+ public HttpResponse checkServiceForConfigConvergence(ApplicationId applicationId, String hostAndPort, URI uri,
+ Duration timeout, Optional<Version> vespaVersion) {
+ return convergeChecker.checkService(getApplication(applicationId, vespaVersion), hostAndPort, uri, timeout);
+ }
+
+ public HttpResponse servicesToCheckForConfigConvergence(ApplicationId applicationId, URI uri,
+ Duration timeoutPerService, Optional<Version> vespaVersion) {
+ return convergeChecker.servicesToCheck(getApplication(applicationId, vespaVersion), uri, timeoutPerService);
+ }
+
+ // ---------------- Logs ----------------------------------------------------------------
+
+ public HttpResponse getLogs(ApplicationId applicationId, Optional<String> hostname, String apiParams) {
+ String logServerURI = getLogServerURI(applicationId, hostname) + apiParams;
+ return logRetriever.getLogs(logServerURI);
+ }
+
+ // ---------------- Methods to do call against tester containers in hosted ------------------------------
+
+ public HttpResponse getTesterStatus(ApplicationId applicationId) {
+ return testerClient.getStatus(getTesterHostname(applicationId), getTesterPort(applicationId));
+ }
+
+ public HttpResponse getTesterLog(ApplicationId applicationId, Long after) {
+ return testerClient.getLog(getTesterHostname(applicationId), getTesterPort(applicationId), after);
+ }
+
+ public HttpResponse startTests(ApplicationId applicationId, String suite, byte[] config) {
+ return testerClient.startTests(getTesterHostname(applicationId), getTesterPort(applicationId), suite, config);
+ }
+
+ public HttpResponse isTesterReady(ApplicationId applicationId) {
+ return testerClient.isTesterReady(getTesterHostname(applicationId), getTesterPort(applicationId));
+ }
+
+ public HttpResponse getTestReport(ApplicationId applicationId) {
+ return testerClient.getReport(getTesterHostname(applicationId), getTesterPort(applicationId));
+ }
+
+ private String getTesterHostname(ApplicationId applicationId) {
+ return getTesterServiceInfo(applicationId).getHostName();
+ }
+
+ private int getTesterPort(ApplicationId applicationId) {
+ ServiceInfo serviceInfo = getTesterServiceInfo(applicationId);
+ return serviceInfo.getPorts().stream().filter(portInfo -> portInfo.getTags().contains("http")).findFirst().get().getPort();
+ }
+
+ private ServiceInfo getTesterServiceInfo(ApplicationId applicationId) {
+ Application application = getApplication(applicationId);
+ return application.getModel().getHosts().stream()
+ .findFirst().orElseThrow(() -> new InternalServerException("Could not find any host for tester app " + applicationId.toFullString()))
+ .getServices().stream()
+ .filter(service -> CONTAINER.serviceName.equals(service.getServiceType()))
+ .findFirst()
+ .orElseThrow(() -> new InternalServerException("Could not find any tester container for tester app " + applicationId.toFullString()));
+ }
+
+ // ---------------- Session operations ----------------------------------------------------------------
+
+
+
+ public CompletionWaiter activate(LocalSession session, Session previousActiveSession, ApplicationId applicationId, boolean ignoreSessionStaleFailure) {
+ CompletionWaiter waiter = session.getSessionZooKeeperClient().createActiveWaiter();
+ NestedTransaction transaction = new NestedTransaction();
+ transaction.add(deactivateCurrentActivateNew(previousActiveSession, session, ignoreSessionStaleFailure));
+ hostProvisioner.ifPresent(provisioner -> provisioner.activate(transaction, applicationId, session.getAllocatedHosts().getHosts()));
+ transaction.commit();
+ return waiter;
+ }
+
+ /**
+ * 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 RemoteSession getActiveSession(ApplicationId applicationId) {
+ return getActiveSession(getTenant(applicationId), applicationId);
+ }
+
+ public long getSessionIdForApplication(ApplicationId applicationId) {
+ Tenant tenant = getTenant(applicationId);
+ if (tenant == null) throw new NotFoundException("Tenant '" + applicationId.tenant() + "' not found");
+ return getSessionIdForApplication(tenant, applicationId);
+ }
+
+ private long getSessionIdForApplication(Tenant tenant, ApplicationId applicationId) {
+ TenantApplications applicationRepo = tenant.getApplicationRepo();
+ if (! applicationRepo.exists(applicationId))
+ throw new NotFoundException("Unknown application id '" + applicationId + "'");
+ return applicationRepo.requireActiveSessionOf(applicationId);
+ }
+
+ public void validateThatSessionIsNotActive(Tenant tenant, long sessionId) {
+ Session session = getRemoteSession(tenant, sessionId);
+ if (Session.Status.ACTIVATE.equals(session.getStatus())) {
+ throw new IllegalStateException("Session is active: " + sessionId);
+ }
+ }
+
+ public void validateThatSessionIsPrepared(Tenant tenant, long sessionId) {
+ Session session = getRemoteSession(tenant, sessionId);
+ if ( ! Session.Status.PREPARE.equals(session.getStatus()))
+ throw new IllegalStateException("Session not prepared: " + sessionId);
+ }
+
+ public long createSessionFromExisting(ApplicationId applicationId,
+ DeployLogger logger,
+ boolean internalRedeploy,
+ TimeoutBudget timeoutBudget) {
+ Tenant tenant = getTenant(applicationId);
+ SessionRepository sessionRepository = tenant.getSessionRepository();
+ RemoteSession fromSession = getExistingSession(tenant, applicationId);
+ LocalSession session = sessionRepository.createSessionFromExisting(fromSession, logger, internalRedeploy, timeoutBudget);
+ sessionRepository.addLocalSession(session);
+ return session.getSessionId();
+ }
+
+ public long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, InputStream in, String contentType) {
+ File tempDir = uncheck(() -> Files.createTempDirectory("deploy")).toFile();
+ long sessionId;
+ try {
+ sessionId = createSession(applicationId, timeoutBudget, decompressApplication(in, contentType, tempDir));
+ } finally {
+ cleanupTempDirectory(tempDir);
+ }
+ return sessionId;
+ }
+
+ public long createSession(ApplicationId applicationId, TimeoutBudget timeoutBudget, File applicationDirectory) {
+ Tenant tenant = getTenant(applicationId);
+ tenant.getApplicationRepo().createApplication(applicationId);
+ Optional<Long> activeSessionId = tenant.getApplicationRepo().activeSessionOf(applicationId);
+ LocalSession session = tenant.getSessionRepository().createSession(applicationDirectory,
+ applicationId,
+ timeoutBudget,
+ activeSessionId);
+ tenant.getSessionRepository().addLocalSession(session);
+ return session.getSessionId();
+ }
+
+ public void deleteExpiredLocalSessions() {
+ Map<Tenant, List<LocalSession>> sessionsPerTenant = new HashMap<>();
+ tenantRepository.getAllTenants().forEach(tenant -> sessionsPerTenant.put(tenant, tenant.getSessionRepository().getLocalSessions()));
+
+ Set<ApplicationId> applicationIds = new HashSet<>();
+ sessionsPerTenant.values()
+ .forEach(sessionList -> sessionList.stream()
+ .map(Session::getOptionalApplicationId)
+ .filter(Optional::isPresent)
+ .forEach(appId -> applicationIds.add(appId.get())));
+
+ Map<ApplicationId, Long> activeSessions = new HashMap<>();
+ applicationIds.forEach(applicationId -> {
+ RemoteSession activeSession = getActiveSession(applicationId);
+ if (activeSession != null)
+ activeSessions.put(applicationId, activeSession.getSessionId());
+ });
+ sessionsPerTenant.keySet().forEach(tenant -> tenant.getSessionRepository().deleteExpiredSessions(activeSessions));
+ }
+
+ public int deleteExpiredSessionLocks(Duration expiryTime) {
+ return tenantRepository.getAllTenants()
+ .stream()
+ .map(tenant -> tenant.getSessionRepository().deleteExpiredLocks(clock, expiryTime))
+ .mapToInt(i -> i)
+ .sum();
+ }
+
+ public int deleteExpiredRemoteSessions(Duration expiryTime) {
+ return deleteExpiredRemoteSessions(clock, expiryTime);
+ }
+
+ public int deleteExpiredRemoteSessions(Clock clock, Duration expiryTime) {
+ return tenantRepository.getAllTenants()
+ .stream()
+ .map(tenant -> tenant.getSessionRepository().deleteExpiredRemoteSessions(clock, expiryTime))
+ .mapToInt(i -> i)
+ .sum();
+ }
+
+ // ---------------- Tenant operations ----------------------------------------------------------------
+
+
+ public TenantRepository tenantRepository() {
+ return tenantRepository;
+ }
+
+ public Set<TenantName> deleteUnusedTenants(Duration ttlForUnusedTenant, Instant now) {
+ return tenantRepository.getAllTenantNames().stream()
+ .filter(tenantName -> activeApplications(tenantName).isEmpty())
+ .filter(tenantName -> !tenantName.equals(TenantName.defaultName())) // Not allowed to remove 'default' tenant
+ .filter(tenantName -> !tenantName.equals(HOSTED_VESPA_TENANT)) // Not allowed to remove 'hosted-vespa' tenant
+ .filter(tenantName -> getTenantMetaData(tenantRepository.getTenant(tenantName)).lastDeployTimestamp().isBefore(now.minus(ttlForUnusedTenant)))
+ .peek(tenantRepository::deleteTenant)
+ .collect(Collectors.toSet());
+ }
+
+ public void deleteTenant(TenantName tenantName) {
+ List<ApplicationId> activeApplications = activeApplications(tenantName);
+ if (activeApplications.isEmpty())
+ tenantRepository.deleteTenant(tenantName);
+ else
+ throw new IllegalArgumentException("Cannot delete tenant '" + tenantName + "', it has active applications: " + activeApplications);
+ }
+
+ private List<ApplicationId> activeApplications(TenantName tenantName) {
+ return tenantRepository.getTenant(tenantName).getApplicationRepo().activeApplications();
+ }
+ // ---------------- Proton Metrics V1 ------------------------------------------------------------------------
+
+ public ProtonMetricsResponse getProtonMetrics(ApplicationId applicationId) {
+ Application application = getApplication(applicationId);
+ ProtonMetricsRetriever protonMetricsRetriever = new ProtonMetricsRetriever();
+ return protonMetricsRetriever.getMetrics(application);
+ }
+
+
+ // ---------------- Deployment Metrics V1 ------------------------------------------------------------------------
+
+ public DeploymentMetricsResponse getDeploymentMetrics(ApplicationId applicationId) {
+ Application application = getApplication(applicationId);
+ DeploymentMetricsRetriever deploymentMetricsRetriever = new DeploymentMetricsRetriever();
+ return deploymentMetricsRetriever.getMetrics(application);
+ }
+
+ // ---------------- Misc operations ----------------------------------------------------------------
+
+ public ApplicationMetaData getMetadataFromLocalSession(Tenant tenant, long sessionId) {
+ return getLocalSession(tenant, sessionId).getMetaData();
+ }
+
+ public ConfigserverConfig configserverConfig() {
+ return configserverConfig;
+ }
+
+ public ApplicationId getApplicationIdForHostname(String hostname) {
+ Optional<ApplicationId> applicationId = tenantRepository.getAllTenantNames().stream()
+ .map(tenantName -> tenantRepository.getTenant(tenantName).getApplicationRepo().getApplicationIdForHostName(hostname))
+ .filter(Objects::nonNull)
+ .findFirst();
+ return applicationId.orElse(null);
+ }
+
+ private void validateThatLocalSessionIsNotActive(Tenant tenant, long sessionId) {
+ LocalSession session = getLocalSession(tenant, sessionId);
+ if (Session.Status.ACTIVATE.equals(session.getStatus())) {
+ throw new IllegalStateException("Session is active: " + sessionId);
+ }
+ }
+
+ private LocalSession getLocalSession(Tenant tenant, long sessionId) {
+ LocalSession session = tenant.getSessionRepository().getLocalSession(sessionId);
+ if (session == null) throw new NotFoundException("Session " + sessionId + " was not found");
+
+ return session;
+ }
+
+ private RemoteSession getRemoteSession(Tenant tenant, long sessionId) {
+ RemoteSession session = tenant.getSessionRepository().getRemoteSession(sessionId);
+ if (session == null) throw new NotFoundException("Session " + sessionId + " was not found");
+
+ return session;
+ }
+
+ public Optional<ApplicationSet> getCurrentActiveApplicationSet(Tenant tenant, ApplicationId appId) {
+ Optional<ApplicationSet> currentActiveApplicationSet = Optional.empty();
+ TenantApplications applicationRepo = tenant.getApplicationRepo();
+ try {
+ long currentActiveSessionId = applicationRepo.requireActiveSessionOf(appId);
+ RemoteSession currentActiveSession = getRemoteSession(tenant, currentActiveSessionId);
+ currentActiveApplicationSet = Optional.ofNullable(tenant.getSessionRepository().ensureApplicationLoaded(currentActiveSession));
+ } catch (IllegalArgumentException e) {
+ // Do nothing if we have no currently active session
+ }
+ return currentActiveApplicationSet;
+ }
+
+ private File decompressApplication(InputStream in, String contentType, File tempDir) {
+ try (CompressedApplicationInputStream application =
+ CompressedApplicationInputStream.createFromCompressedStream(in, contentType)) {
+ return decompressApplication(application, tempDir);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Unable to decompress data in body", e);
+ }
+ }
+
+ private File decompressApplication(CompressedApplicationInputStream in, File tempDir) {
+ try {
+ return in.decompress(tempDir);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Unable to decompress stream", e);
+ }
+ }
+
+ private void cleanupTempDirectory(File tempDir) {
+ logger.log(Level.FINE, "Deleting tmp dir '" + tempDir + "'");
+ if (!IOUtils.recursiveDeleteDir(tempDir)) {
+ logger.log(Level.WARNING, "Not able to delete tmp dir '" + tempDir + "'");
+ }
+ }
+
+ private RemoteSession getExistingSession(Tenant tenant, ApplicationId applicationId) {
+ TenantApplications applicationRepo = tenant.getApplicationRepo();
+ return getRemoteSession(tenant, applicationRepo.requireActiveSessionOf(applicationId));
+ }
+
+ private RemoteSession getActiveSession(Tenant tenant, ApplicationId applicationId) {
+ TenantApplications applicationRepo = tenant.getApplicationRepo();
+ if (applicationRepo.activeApplications().contains(applicationId)) {
+ return tenant.getSessionRepository().getRemoteSession(applicationRepo.requireActiveSessionOf(applicationId));
+ }
+ return null;
+ }
+
+ public LocalSession getActiveLocalSession(Tenant tenant, ApplicationId applicationId) {
+ TenantApplications applicationRepo = tenant.getApplicationRepo();
+ if (applicationRepo.activeApplications().contains(applicationId)) {
+ return tenant.getSessionRepository().getLocalSession(applicationRepo.requireActiveSessionOf(applicationId));
+ }
+ return null;
+ }
+
+ private static void logConfigChangeActions(ConfigChangeActions actions, DeployLogger logger) {
+ RestartActions restartActions = actions.getRestartActions();
+ if ( ! restartActions.isEmpty()) {
+ logger.log(Level.WARNING, "Change(s) between active and new application that require restart:\n" +
+ restartActions.format());
+ }
+ RefeedActions refeedActions = actions.getRefeedActions();
+ if ( ! refeedActions.isEmpty()) {
+ boolean allAllowed = refeedActions.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" +
+ refeedActions.format());
+ }
+ }
+
+ private String getLogServerURI(ApplicationId applicationId, Optional<String> hostname) {
+ // Allow to get logs from a given hostname if the application is under the hosted-vespa tenant.
+ // We make no validation that the hostname is actually allocated to the given application since
+ // most applications under hosted-vespa are not known to the model and it's OK for a user to get
+ // logs for any host if they are authorized for the hosted-vespa tenant.
+ if (hostname.isPresent() && HOSTED_VESPA_TENANT.equals(applicationId.tenant())) {
+ return "http://" + hostname.get() + ":8080/logs";
+ }
+
+ Application application = getApplication(applicationId);
+ Collection<HostInfo> hostInfos = application.getModel().getHosts();
+
+ HostInfo logServerHostInfo = hostInfos.stream()
+ .filter(host -> host.getServices().stream()
+ .anyMatch(serviceInfo -> serviceInfo.getServiceType().equalsIgnoreCase("logserver")))
+ .findFirst().orElseThrow(() -> new IllegalArgumentException("Could not find host info for logserver"));
+
+ ServiceInfo serviceInfo = logServerHostInfo.getServices().stream().filter(service -> List.of(LOGSERVER_CONTAINER.serviceName, CONTAINER.serviceName).contains(service.getServiceType()))
+ .findFirst().orElseThrow(() -> new IllegalArgumentException("No container running on logserver host"));
+ int port = servicePort(serviceInfo);
+ return "http://" + logServerHostInfo.getHostname() + ":" + port + "/logs";
+ }
+
+ private int servicePort(ServiceInfo serviceInfo) {
+ return serviceInfo.getPorts().stream()
+ .filter(portInfo -> portInfo.getTags().stream().anyMatch(tag -> tag.equalsIgnoreCase("http")))
+ .findFirst().orElseThrow(() -> new IllegalArgumentException("Could not find HTTP port"))
+ .getPort();
+ }
+
+ public Slime createDeployLog() {
+ Slime deployLog = new Slime();
+ deployLog.setObject();
+ return deployLog;
+ }
+
+ public Zone zone() {
+ return new Zone(SystemName.from(configserverConfig.system()),
+ Environment.from(configserverConfig.environment()),
+ RegionName.from(configserverConfig.region()));
+ }
+
+ /** Emits as a metric the time in millis spent while holding this timer, with deployment ID as dimensions. */
+ public ActionTimer timerFor(ApplicationId id, String metricName) {
+ return new ActionTimer(metric, clock, id, configserverConfig.environment(), configserverConfig.region(), metricName);
+ }
+
+ public static class ActionTimer implements AutoCloseable {
+
+ private final Metric metric;
+ private final Clock clock;
+ private final ApplicationId id;
+ private final String environment;
+ private final String region;
+ private final String name;
+ private final Instant start;
+
+ private ActionTimer(Metric metric, Clock clock, ApplicationId id, String environment, String region, String name) {
+ this.metric = metric;
+ this.clock = clock;
+ this.id = id;
+ this.environment = environment;
+ this.region = region;
+ this.name = name;
+ this.start = clock.instant();
+ }
+
+ @Override
+ public void close() {
+ metric.set(name,
+ Duration.between(start, clock.instant()).toMillis(),
+ metric.createContext(Map.of("applicationId", id.toFullString(),
+ "tenantName", id.tenant().value(),
+ "app", id.application().value() + "." + id.instance().value(),
+ "zone", environment + "." + region)));
+ }
+
+ }
+
+}
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
index 7fc6b35722f..6d2ef4028c6 100644
--- 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
@@ -4,7 +4,6 @@ package com.yahoo.vespa.config.server.modelfactory;
import com.google.common.collect.ImmutableSet;
import com.yahoo.component.Version;
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.ModelContext;
import com.yahoo.config.model.api.ModelFactory;
@@ -57,7 +56,6 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
private final ConfigDefinitionRepo configDefinitionRepo;
private final Metrics metrics;
private final Curator curator;
- private final DeployLogger logger;
private final FlagSource flagSource;
private final SecretStore secretStore;
@@ -76,7 +74,6 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
this.configDefinitionRepo = globalComponentRegistry.getStaticConfigDefinitionRepo();
this.metrics = globalComponentRegistry.getMetrics();
this.curator = globalComponentRegistry.getCurator();
- this.logger = new SilentDeployLogger();
this.flagSource = globalComponentRegistry.getFlagSource();
this.secretStore = globalComponentRegistry.getSecretStore();
}
@@ -90,14 +87,14 @@ public class ActivatedModelsBuilder extends ModelsBuilder<Application> {
Optional<AllocatedHosts> ignored // Ignored since we have this in the app package for activated models
) {
log.log(Level.FINE, String.format("Loading model version %s for session %s application %s",
- modelFactory.version(), appGeneration, applicationId));
+ modelFactory.version(), appGeneration, applicationId));
ModelContext.Properties modelContextProperties = createModelContextProperties(applicationId);
Provisioned provisioned = new Provisioned();
ModelContext modelContext = new ModelContextImpl(
applicationPackage,
Optional.empty(),
permanentApplicationPackage.applicationPackage(),
- logger,
+ new SilentDeployLogger(),
configDefinitionRepo,
getForVersionOrLatest(applicationPackage.getFileRegistries(), modelFactory.version()).orElse(new MockFileRegistry()),
createStaticProvisioner(applicationPackage.getAllocatedHosts(),
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
index 36cac87a326..ed401e65f20 100644
--- 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
@@ -1,22 +1,11 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.config.server.session;
-import com.yahoo.config.application.api.ApplicationPackage;
-import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.TenantName;
-import com.yahoo.lang.SettableOptional;
import com.yahoo.transaction.Transaction;
-import com.yahoo.vespa.config.server.GlobalComponentRegistry;
import com.yahoo.vespa.config.server.application.ApplicationSet;
-import com.yahoo.vespa.config.server.modelfactory.ActivatedModelsBuilder;
-import com.yahoo.vespa.curator.Curator;
-import org.apache.zookeeper.KeeperException;
-import java.time.Clock;
import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
/**
* A RemoteSession represents a session created on another config server. This session can
@@ -26,95 +15,46 @@ import java.util.logging.Logger;
*/
public class RemoteSession extends Session {
- private static final Logger log = Logger.getLogger(RemoteSession.class.getName());
- private ApplicationSet applicationSet = null;
- private final ActivatedModelsBuilder applicationLoader;
- private final Clock clock;
+ private final Optional<ApplicationSet> applicationSet;
/**
- * Creates a session. This involves loading the application, validating it and distributing it.
+ * Creates a remote session, no application set loaded
*
* @param tenant The name of the tenant creating session
* @param sessionId The session id for this session.
- * @param componentRegistry a registry of global components
* @param zooKeeperClient a SessionZooKeeperClient instance
*/
public RemoteSession(TenantName tenant,
long sessionId,
- GlobalComponentRegistry componentRegistry,
SessionZooKeeperClient zooKeeperClient) {
- super(tenant, sessionId, zooKeeperClient);
- this.applicationLoader = new ActivatedModelsBuilder(tenant, sessionId, zooKeeperClient, componentRegistry);
- this.clock = componentRegistry.getClock();
- }
-
- void prepare() {
- Curator.CompletionWaiter waiter = sessionZooKeeperClient.getPrepareWaiter();
- ensureApplicationLoaded();
- notifyCompletion(waiter);
+ this(tenant, sessionId, zooKeeperClient, Optional.empty());
}
- private ApplicationSet loadApplication() {
- ApplicationPackage applicationPackage = sessionZooKeeperClient.loadApplicationPackage();
-
- // Read hosts allocated on the config server instance which created this
- Optional<AllocatedHosts> allocatedHosts = applicationPackage.getAllocatedHosts();
-
- return ApplicationSet.fromList(applicationLoader.buildModels(getApplicationId(),
- sessionZooKeeperClient.readDockerImageRepository(),
- sessionZooKeeperClient.readVespaVersion(),
- applicationPackage,
- new SettableOptional<>(allocatedHosts),
- clock.instant()));
+ /**
+ * Creates a remote session, with application set
+ *
+ * @param tenant The name of the tenant creating session
+ * @param sessionId The session id for this session.
+ * @param zooKeeperClient a SessionZooKeeperClient instance
+ */
+ public RemoteSession(TenantName tenant,
+ long sessionId,
+ SessionZooKeeperClient zooKeeperClient,
+ Optional<ApplicationSet> applicationSet) {
+ super(tenant, sessionId, zooKeeperClient);
+ this.applicationSet = applicationSet;
}
- public synchronized ApplicationSet ensureApplicationLoaded() {
- return applicationSet == null ? applicationSet = loadApplication() : applicationSet;
+ Optional<ApplicationSet> applicationSet() {
+ return applicationSet;
}
- public synchronized void deactivate() {
- applicationSet = null;
+ public synchronized RemoteSession deactivated() {
+ return new RemoteSession(tenant, sessionId, sessionZooKeeperClient, Optional.empty());
}
public Transaction createDeleteTransaction() {
return sessionZooKeeperClient.createWriteStatusTransaction(Status.DELETE);
}
-
- void confirmUpload() {
- Curator.CompletionWaiter waiter = sessionZooKeeperClient.getUploadWaiter();
- log.log(Level.FINE, "Notifying upload waiter for session " + getSessionId());
- notifyCompletion(waiter);
- log.log(Level.FINE, "Done notifying upload for session " + getSessionId());
- }
-
- void notifyCompletion(Curator.CompletionWaiter completionWaiter) {
- try {
- completionWaiter.notifyCompletion();
- } catch (RuntimeException e) {
- // Throw only if we get something else than NoNodeException or NodeExistsException.
- // NoNodeException might happen when the session is no longer in use (e.g. the app using this session
- // has been deleted) and this method has not been called yet for the previous session operation on a
- // minority of the config servers.
- // NodeExistsException might happen if an event for this node is delivered more than once, in that case
- // this is a no-op
- Set<Class<? extends KeeperException>> acceptedExceptions = Set.of(KeeperException.NoNodeException.class,
- KeeperException.NodeExistsException.class);
- Class<? extends Throwable> exceptionClass = e.getCause().getClass();
- if (acceptedExceptions.contains(exceptionClass))
- log.log(Level.FINE, "Not able to notify completion for session " + getSessionId() +
- " (" + completionWaiter + ")," +
- " node " + (exceptionClass.equals(KeeperException.NoNodeException.class)
- ? "has been deleted"
- : "already exists"));
- else
- throw e;
- }
- }
-
- public void delete() {
- Transaction transaction = sessionZooKeeperClient.deleteTransaction();
- transaction.commit();
- transaction.close();
- }
}
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
index b3e35e955de..85f9b575942 100644
--- 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
@@ -28,7 +28,7 @@ import java.util.Optional;
*/
public abstract class Session implements Comparable<Session> {
- private final long sessionId;
+ protected final long sessionId;
protected final TenantName tenant;
protected final SessionZooKeeperClient sessionZooKeeperClient;
protected final Optional<ApplicationPackage> applicationPackage;
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionCache.java
index b7d78f11201..9cf0b1e428d 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionCache.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionCache.java
@@ -15,7 +15,7 @@ public class SessionCache<SESSIONTYPE extends Session> {
private final HashMap<Long, SESSIONTYPE> sessions = new HashMap<>();
- public synchronized void addSession(SESSIONTYPE session) {
+ public synchronized void putSession(SESSIONTYPE session) {
sessions.putIfAbsent(session.getSessionId(), session);
}
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java
index cbfa59b26e4..d71b8f4d46d 100644
--- a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java
+++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepository.java
@@ -8,9 +8,11 @@ import com.yahoo.config.application.api.ApplicationPackage;
import com.yahoo.config.application.api.DeployLogger;
import com.yahoo.config.model.application.provider.DeployData;
import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.provision.AllocatedHosts;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
import com.yahoo.io.IOUtils;
+import com.yahoo.lang.SettableOptional;
import com.yahoo.path.Path;
import com.yahoo.transaction.AbstractTransaction;
import com.yahoo.transaction.NestedTransaction;
@@ -22,6 +24,7 @@ import com.yahoo.vespa.config.server.application.TenantApplications;
import com.yahoo.vespa.config.server.configchange.ConfigChangeActions;
import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs;
import com.yahoo.vespa.config.server.filedistribution.FileDirectory;
+import com.yahoo.vespa.config.server.modelfactory.ActivatedModelsBuilder;
import com.yahoo.vespa.config.server.monitoring.MetricUpdater;
import com.yahoo.vespa.config.server.monitoring.Metrics;
import com.yahoo.vespa.config.server.tenant.TenantRepository;
@@ -36,6 +39,7 @@ import com.yahoo.vespa.flags.Flags;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.zookeeper.KeeperException;
import java.io.File;
import java.io.FilenameFilter;
@@ -51,6 +55,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -121,7 +126,7 @@ public class SessionRepository {
// ---------------- Local sessions ----------------------------------------------------------------
public synchronized void addLocalSession(LocalSession session) {
- localSessionCache.addSession(session);
+ localSessionCache.putSession(session);
long sessionId = session.getSessionId();
RemoteSession remoteSession = createRemoteSession(sessionId);
addSessionStateWatcher(sessionId, remoteSession, Optional.of(session));
@@ -263,7 +268,7 @@ public class SessionRepository {
}
public void addRemoteSession(RemoteSession session) {
- remoteSessionCache.addSession(session);
+ remoteSessionCache.putSession(session);
metrics.incAddedSessions();
}
@@ -275,13 +280,24 @@ public class SessionRepository {
if (session.getStatus() == Session.Status.ACTIVATE) continue;
if (sessionHasExpired(session.getCreateTime(), expiryTime, clock)) {
log.log(Level.FINE, () -> "Remote session " + sessionId + " for " + tenantName + " has expired, deleting it");
- session.delete();
+ deleteSession(session);
deleted++;
}
}
return deleted;
}
+ public void deactivate(RemoteSession remoteSession) {
+ remoteSessionCache.putSession(remoteSession.deactivated());
+ }
+
+ public void deleteSession(RemoteSession session) {
+ SessionZooKeeperClient sessionZooKeeperClient = createSessionZooKeeperClient(session.getSessionId());
+ Transaction transaction = sessionZooKeeperClient.deleteTransaction();
+ transaction.commit();
+ transaction.close();
+ }
+
public int deleteExpiredLocks(Clock clock, Duration expiryTime) {
int deleted = 0;
for (var lock : curator.getChildren(locksPath)) {
@@ -358,25 +374,17 @@ public class SessionRepository {
long sessionId = session.getSessionId();
Curator.CompletionWaiter waiter = createSessionZooKeeperClient(sessionId).getActiveWaiter();
log.log(Level.FINE, () -> session.logPre() + "Getting session from repo: " + sessionId);
- ApplicationSet app = session.ensureApplicationLoaded();
+ ApplicationSet app = ensureApplicationLoaded(session);
log.log(Level.FINE, () -> session.logPre() + "Reloading config for " + sessionId);
applicationRepo.reloadConfig(app);
log.log(Level.FINE, () -> session.logPre() + "Notifying " + waiter);
- session.notifyCompletion(waiter);
+ notifyCompletion(waiter, session);
log.log(Level.INFO, session.logPre() + "Session activated: " + sessionId);
}
- public void deactivate(RemoteSession remoteSession) {
- remoteSession.deactivate();
- }
-
- public void delete(RemoteSession remoteSession, Optional<LocalSession> localSession) {
+ void deleteSession(RemoteSession remoteSession, Optional<LocalSession> localSession) {
localSession.ifPresent(this::deleteLocalSession);
- remoteSession.deactivate();
- }
-
- void prepare(RemoteSession session) {
- session.prepare();
+ deactivate(remoteSession);
}
boolean distributeApplicationPackage() {
@@ -394,13 +402,84 @@ public class SessionRepository {
for (ApplicationId applicationId : applicationRepo.activeApplications()) {
if (applicationRepo.requireActiveSessionOf(applicationId) == session.getSessionId()) {
log.log(Level.FINE, () -> "Found active application for session " + session.getSessionId() + " , loading it");
- applicationRepo.reloadConfig(session.ensureApplicationLoaded());
+ applicationRepo.reloadConfig(ensureApplicationLoaded(session));
log.log(Level.INFO, session.logPre() + "Application activated successfully: " + applicationId + " (generation " + session.getSessionId() + ")");
return;
}
}
}
+ void prepareRemoteSession(RemoteSession session) {
+ SessionZooKeeperClient sessionZooKeeperClient = createSessionZooKeeperClient(session.getSessionId());
+ Curator.CompletionWaiter waiter = sessionZooKeeperClient.getPrepareWaiter();
+ ensureApplicationLoaded(session);
+ notifyCompletion(waiter, session);
+ }
+
+ public ApplicationSet ensureApplicationLoaded(RemoteSession session) {
+ Optional<ApplicationSet> applicationSet = session.applicationSet();
+ if (applicationSet.isPresent()) {
+ return applicationSet.get();
+ }
+
+ ApplicationSet newApplicationSet = loadApplication(session);
+ RemoteSession newSession = new RemoteSession(session.getTenantName(),
+ session.getSessionId(),
+ session.getSessionZooKeeperClient(),
+ Optional.of(newApplicationSet));
+ remoteSessionCache.putSession(newSession);
+ return newApplicationSet;
+ }
+
+ void confirmUpload(RemoteSession session) {
+ Curator.CompletionWaiter waiter = session.getSessionZooKeeperClient().getUploadWaiter();
+ long sessionId = session.getSessionId();
+ log.log(Level.FINE, "Notifying upload waiter for session " + sessionId);
+ notifyCompletion(waiter, session);
+ log.log(Level.FINE, "Done notifying upload for session " + sessionId);
+ }
+
+ void notifyCompletion(Curator.CompletionWaiter completionWaiter, RemoteSession session) {
+ try {
+ completionWaiter.notifyCompletion();
+ } catch (RuntimeException e) {
+ // Throw only if we get something else than NoNodeException or NodeExistsException.
+ // NoNodeException might happen when the session is no longer in use (e.g. the app using this session
+ // has been deleted) and this method has not been called yet for the previous session operation on a
+ // minority of the config servers.
+ // NodeExistsException might happen if an event for this node is delivered more than once, in that case
+ // this is a no-op
+ Set<Class<? extends KeeperException>> acceptedExceptions = Set.of(KeeperException.NoNodeException.class,
+ KeeperException.NodeExistsException.class);
+ Class<? extends Throwable> exceptionClass = e.getCause().getClass();
+ if (acceptedExceptions.contains(exceptionClass))
+ log.log(Level.FINE, "Not able to notify completion for session " + session.getSessionId() +
+ " (" + completionWaiter + ")," +
+ " node " + (exceptionClass.equals(KeeperException.NoNodeException.class)
+ ? "has been deleted"
+ : "already exists"));
+ else
+ throw e;
+ }
+ }
+
+ private ApplicationSet loadApplication(RemoteSession session) {
+ SessionZooKeeperClient sessionZooKeeperClient = createSessionZooKeeperClient(session.getSessionId());
+ ApplicationPackage applicationPackage = sessionZooKeeperClient.loadApplicationPackage();
+ ActivatedModelsBuilder builder = new ActivatedModelsBuilder(session.getTenantName(),
+ session.getSessionId(),
+ sessionZooKeeperClient,
+ componentRegistry);
+ // Read hosts allocated on the config server instance which created this
+ Optional<AllocatedHosts> allocatedHosts = applicationPackage.getAllocatedHosts();
+ return ApplicationSet.fromList(builder.buildModels(session.getApplicationId(),
+ sessionZooKeeperClient.readDockerImageRepository(),
+ sessionZooKeeperClient.readVespaVersion(),
+ applicationPackage,
+ new SettableOptional<>(allocatedHosts),
+ clock.instant()));
+ }
+
private void nodeChanged() {
zkWatcherExecutor.execute(() -> {
Multiset<Session.Status> sessionMetrics = HashMultiset.create();
@@ -436,7 +515,7 @@ public class SessionRepository {
RemoteSession session = remoteSessionCache.getSession(sessionId);
if (session == null) continue; // session might have been deleted after getting session list
log.log(Level.FINE, () -> session.logPre() + "Confirming upload for session " + sessionId);
- session.confirmUpload();
+ confirmUpload(session);
}
}
@@ -455,7 +534,7 @@ public class SessionRepository {
public RemoteSession createRemoteSession(long sessionId) {
SessionZooKeeperClient sessionZKClient = createSessionZooKeeperClient(sessionId);
- return new RemoteSession(tenantName, sessionId, componentRegistry, sessionZKClient);
+ return new RemoteSession(tenantName, sessionId, sessionZKClient);
}
private void ensureSessionPathDoesNotExist(long sessionId) {
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
index 57d9f027447..a00a049a297 100644
--- 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
@@ -55,7 +55,7 @@ public class SessionStateWatcher {
break;
case PREPARE:
createLocalSession(sessionId);
- sessionRepository.prepare(remoteSession);
+ sessionRepository.prepareRemoteSession(remoteSession);
break;
case ACTIVATE:
createLocalSession(sessionId);
@@ -65,7 +65,7 @@ public class SessionStateWatcher {
sessionRepository.deactivate(remoteSession);
break;
case DELETE:
- sessionRepository.delete(remoteSession, localSession);
+ sessionRepository.deleteSession(remoteSession, localSession);
break;
default:
throw new IllegalStateException("Unknown status " + newStatus);
diff --git a/configserver/src/test/apps/app-major-version-2/deployment.xml b/configserver/src/test/apps/app-major-version-2/deployment.xml
new file mode 100644
index 00000000000..7523c104b7e
--- /dev/null
+++ b/configserver/src/test/apps/app-major-version-2/deployment.xml
@@ -0,0 +1 @@
+<deployment version='1.0' major-version='2'/>
diff --git a/configserver/src/test/apps/app-major-version-2/hosts.xml b/configserver/src/test/apps/app-major-version-2/hosts.xml
new file mode 100644
index 00000000000..f4256c9fc81
--- /dev/null
+++ b/configserver/src/test/apps/app-major-version-2/hosts.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<hosts>
+ <host name="mytesthost">
+ <alias>node1</alias>
+ </host>
+</hosts>
diff --git a/configserver/src/test/apps/app-major-version-2/searchdefinitions/music.sd b/configserver/src/test/apps/app-major-version-2/searchdefinitions/music.sd
new file mode 100644
index 00000000000..7670e78f22b
--- /dev/null
+++ b/configserver/src/test/apps/app-major-version-2/searchdefinitions/music.sd
@@ -0,0 +1,50 @@
+# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# A basic search definition - called music, should be saved to music.sd
+search music {
+
+ # It contains one document type only - called music as well
+ document music {
+
+ field title type string {
+ indexing: summary | index # How this field should be indexed
+ # index-to: title, default # Create two indexes
+ weight: 75 # Ranking importancy of this field, used by the built in nativeRank feature
+ }
+
+ field artist type string {
+ indexing: summary | attribute | index
+ # index-to: artist, default
+
+ weight: 25
+ }
+
+ field year type int {
+ indexing: summary | attribute
+ }
+
+ # Increase query
+ field popularity type int {
+ indexing: summary | attribute
+ }
+
+ field url type uri {
+ indexing: summary | index
+ }
+
+ }
+
+ rank-profile default inherits default {
+ first-phase {
+ expression: nativeRank(title,artist) + attribute(popularity)
+ }
+
+ }
+
+ rank-profile textmatch inherits default {
+ first-phase {
+ expression: nativeRank(title,artist)
+ }
+
+ }
+
+}
diff --git a/configserver/src/test/apps/app-major-version-2/services.xml b/configserver/src/test/apps/app-major-version-2/services.xml
new file mode 100644
index 00000000000..509d7786be0
--- /dev/null
+++ b/configserver/src/test/apps/app-major-version-2/services.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!-- Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<services version="1.0">
+
+ <admin version="2.0">
+ <adminserver hostalias="node1"/>
+ <logserver hostalias="node1" />
+ </admin>
+
+ <content version="1.0">
+ <redundancy>2</redundancy>
+ <documents>
+ <document type="music" mode="index"/>
+ </documents>
+ <nodes>
+ <node hostalias="node1" distribution-key="0"/>
+ </nodes>
+
+ </content>
+
+ <container version="1.0">
+ <document-processing compressdocuments="true">
+ <chain id="ContainerWrapperTest">
+ <documentprocessor id="com.yahoo.vespa.config.AppleDocProc"/>
+ </chain>
+ </document-processing>
+
+ <config name="project.specific">
+ <value>someval</value>
+ </config>
+
+ <nodes>
+ <node hostalias="node1" />
+ </nodes>
+
+ </container>
+
+</services>
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
index d84b81bd8e7..b7310917449 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandlerTest.java
@@ -226,7 +226,7 @@ public class ApplicationHandlerTest {
ApplicationId unknown = new ApplicationId.Builder().applicationName("unknown").tenant("default").build();
HttpResponse responseForUnknown = fileDistributionStatus(unknown, zone);
assertEquals(404, responseForUnknown.getStatus());
- assertEquals("{\"error-code\":\"NOT_FOUND\",\"message\":\"Unknown application id 'default.unknown'\"}",
+ assertEquals("{\"error-code\":\"NOT_FOUND\",\"message\":\"No active session found for 'default.unknown'\"}",
getRenderedString(responseForUnknown));
}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcServerTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcServerTest.java
index ae6bd5feeab..d8d20d37551 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcServerTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/rpc/RpcServerTest.java
@@ -64,7 +64,8 @@ public class RpcServerTest {
ApplicationRepository applicationRepository = tester.applicationRepository();
applicationRepository.deploy(testApp, new PrepareParams.Builder().applicationId(applicationId).build());
TenantApplications applicationRepo = tester.tenant().getApplicationRepo();
- applicationRepo.reloadConfig(applicationRepository.getActiveSession(applicationId).ensureApplicationLoaded());
+ ApplicationSet applicationSet = tester.tenant().getSessionRepository().ensureApplicationLoaded(applicationRepository.getActiveSession(applicationId));
+ applicationRepo.reloadConfig(applicationSet);
testPrintStatistics(tester);
testGetConfig(tester);
testEnabled(tester);
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java
deleted file mode 100644
index cda3e09d3a8..00000000000
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/RemoteSessionTest.java
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.config.server.session;
-
-import com.yahoo.cloud.config.ConfigserverConfig;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.ApplicationPackage;
-import com.yahoo.config.model.NullConfigModelRegistry;
-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.api.ValidationParameters;
-import com.yahoo.config.model.deploy.DeployState;
-import com.yahoo.config.model.test.MockApplicationPackage;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.config.server.TestComponentRegistry;
-import com.yahoo.vespa.config.server.application.ApplicationSet;
-import com.yahoo.vespa.config.server.application.PermanentApplicationPackage;
-import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
-import com.yahoo.vespa.curator.Curator;
-import com.yahoo.vespa.curator.mock.MockCurator;
-import com.yahoo.vespa.model.VespaModel;
-import com.yahoo.vespa.model.VespaModelFactory;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-
-import java.io.IOException;
-import java.time.Clock;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-import static org.hamcrest.core.Is.is;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
-
-/**
- * @author Ulf Lilleengen
- * @author bratseth
- */
-public class RemoteSessionTest {
-
- private static final TenantName tenantName = TenantName.from("default");
-
- private Curator curator;
-
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- @Before
- public void setupTest() {
- curator = new MockCurator();
- }
-
- @Test
- public void require_that_session_is_initialized() {
- Clock clock = Clock.systemUTC();
- Session session = createSession(2, clock);
- assertThat(session.getSessionId(), is(2L));
- session = createSession(Long.MAX_VALUE, clock);
- assertThat(session.getSessionId(), is(Long.MAX_VALUE));
- }
-
- @Test
- public void require_that_applications_are_loaded() {
- RemoteSession session = createSession(3, Arrays.asList(new MockModelFactory(), new VespaModelFactory(new NullConfigModelRegistry())));
- session.prepare();
- ApplicationSet applicationSet = session.ensureApplicationLoaded();
- assertNotNull(applicationSet);
- assertThat(applicationSet.getApplicationGeneration(), is(3L));
- assertThat(applicationSet.getForVersionOrLatest(Optional.empty(), Instant.now()).getId().application().value(), is("foo"));
- assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty(), Instant.now()).getModel());
- session.deactivate();
-
- applicationSet = session.ensureApplicationLoaded();
- assertNotNull(applicationSet);
- assertThat(applicationSet.getApplicationGeneration(), is(3L));
- assertThat(applicationSet.getForVersionOrLatest(Optional.empty(), Instant.now()).getId().application().value(), is("foo"));
- assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty(), Instant.now()).getModel());
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void require_that_new_invalid_application_throws_exception() {
- MockModelFactory failingFactory = new MockModelFactory();
- failingFactory.vespaVersion = new Version(1, 2, 0);
- failingFactory.throwOnLoad = true;
-
- MockModelFactory okFactory = new MockModelFactory();
- okFactory.vespaVersion = new Version(1, 1, 0);
- okFactory.throwOnLoad = false;
-
- RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory));
- session.prepare();
- }
-
- @Test
- public void require_that_old_invalid_application_does_not_throw_exception_if_skipped_also_across_major_versions() {
- MockModelFactory failingFactory = new MockModelFactory();
- failingFactory.vespaVersion = new Version(1, 0, 0);
- failingFactory.throwOnLoad = true;
-
- MockModelFactory okFactory =
- new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>");
- okFactory.vespaVersion = new Version(2, 0, 0);
- okFactory.throwOnLoad = false;
-
- RemoteSession session = createSession(3, Arrays.asList(okFactory, failingFactory), failingFactory.clock());
- session.prepare();
- }
-
- @Test
- public void require_that_an_application_package_can_limit_to_one_major_version() {
- ApplicationPackage application =
- new MockApplicationPackage.Builder().withServices("<services version='1.0'/>")
- .withDeploymentSpec("<deployment version='1.0' major-version='2'/>")
- .build();
- assertTrue(application.getMajorVersion().isPresent());
- assertEquals(2, (int)application.getMajorVersion().get());
-
- MockModelFactory failingFactory = new MockModelFactory();
- failingFactory.vespaVersion = new Version(3, 0, 0);
- failingFactory.throwErrorOnLoad = true;
-
- MockModelFactory okFactory = new MockModelFactory();
- okFactory.vespaVersion = new Version(2, 0, 0);
- okFactory.throwErrorOnLoad = false;
-
- SessionZooKeeperClient zkc = new MockSessionZKClient(curator, tenantName, 3, application);
- RemoteSession session = createSession(3, zkc, Arrays.asList(okFactory, failingFactory));
- session.prepare();
-
- // Does not cause an error because model version 3 is skipped
- }
-
- @Test
- public void require_that_an_application_package_can_limit_to_one_higher_major_version() {
- ApplicationPackage application =
- new MockApplicationPackage.Builder().withServices("<services version='1.0'/>")
- .withDeploymentSpec("<deployment version='1.0' major-version='3'/>")
- .build();
- assertTrue(application.getMajorVersion().isPresent());
- assertEquals(3, (int)application.getMajorVersion().get());
-
- MockModelFactory failingFactory = new MockModelFactory();
- failingFactory.vespaVersion = new Version(4, 0, 0);
- failingFactory.throwErrorOnLoad = true;
-
- MockModelFactory okFactory = new MockModelFactory();
- okFactory.vespaVersion = new Version(2, 0, 0);
- okFactory.throwErrorOnLoad = false;
-
- SessionZooKeeperClient zkc = new MockSessionZKClient(curator, tenantName, 3, application);
- RemoteSession session = createSession(4, zkc, Arrays.asList(okFactory, failingFactory));
- session.prepare();
-
- // Does not cause an error because model version 4 is skipped
- }
-
- @Test
- public void require_that_session_status_is_updated() {
- SessionZooKeeperClient zkc = new MockSessionZKClient(curator, tenantName, 3);
- RemoteSession session = createSession(3, zkc, Clock.systemUTC());
- assertThat(session.getStatus(), is(Session.Status.NEW));
- zkc.writeStatus(Session.Status.PREPARE);
- assertThat(session.getStatus(), is(Session.Status.PREPARE));
- }
-
- @Test
- public void require_that_permanent_app_is_used() throws IOException {
- Optional<PermanentApplicationPackage> permanentApp = Optional.of(new PermanentApplicationPackage(
- new ConfigserverConfig(new ConfigserverConfig.Builder()
- .applicationDirectory(temporaryFolder.newFolder("appdir").getAbsolutePath()))));
- MockModelFactory mockModelFactory = new MockModelFactory();
- try {
- int sessionId = 3;
- SessionZooKeeperClient zkc = new MockSessionZKClient(curator, tenantName, sessionId);
- createSession(sessionId, zkc, Collections.singletonList(mockModelFactory), permanentApp, Clock.systemUTC()).ensureApplicationLoaded();
- } catch (Exception e) {
- e.printStackTrace();
- // ignore, we're not interested in deploy errors as long as the below state is OK.
- }
- assertNotNull(mockModelFactory.modelContext);
- assertTrue(mockModelFactory.modelContext.permanentApplicationPackage().isPresent());
- }
-
- private RemoteSession createSession(long sessionId, Clock clock) {
- return createSession(sessionId, Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())), clock);
- }
-
- private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, Clock clock) {
- return createSession(sessionId, zkc, Collections.singletonList(new VespaModelFactory(new NullConfigModelRegistry())), clock);
- }
-
- private RemoteSession createSession(long sessionId, List<ModelFactory> modelFactories) {
- SessionZooKeeperClient zkc = new MockSessionZKClient(curator, tenantName, sessionId);
- return createSession(sessionId, zkc, modelFactories, Clock.systemUTC());
- }
-
- private RemoteSession createSession(long sessionId, List<ModelFactory> modelFactories, Clock clock) {
- SessionZooKeeperClient zkc = new MockSessionZKClient(curator, tenantName, sessionId);
- return createSession(sessionId, zkc, modelFactories, clock);
- }
-
- private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, List<ModelFactory> modelFactories) {
- return createSession(sessionId, zkc, modelFactories, Optional.empty(), Clock.systemUTC());
- }
-
- private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc, List<ModelFactory> modelFactories, Clock clock) {
- return createSession(sessionId, zkc, modelFactories, Optional.empty(), clock);
- }
-
- private RemoteSession createSession(long sessionId, SessionZooKeeperClient zkc,
- List<ModelFactory> modelFactories,
- Optional<PermanentApplicationPackage> permanentApplicationPackage,
- Clock clock) {
- zkc.writeStatus(Session.Status.NEW);
- zkc.writeApplicationId(new ApplicationId.Builder().applicationName("foo").instanceName("bim").build());
- TestComponentRegistry.Builder registryBuilder = new TestComponentRegistry.Builder()
- .curator(curator)
- .clock(clock)
- .modelFactoryRegistry(new ModelFactoryRegistry(modelFactories));
- permanentApplicationPackage.ifPresent(registryBuilder::permanentApplicationPackage);
-
- return new RemoteSession(tenantName, sessionId, registryBuilder.build(), zkc);
- }
-
- private static class MockModelFactory implements ModelFactory {
-
- /** Throw a RuntimeException on load - this is handled gracefully during model building */
- boolean throwOnLoad = false;
-
- /** Throw an Error on load - this is useful to propagate this condition all the way to the test */
- boolean throwErrorOnLoad = false;
-
- ModelContext modelContext;
- public Version vespaVersion = new Version(1, 2, 3);
-
- /** The validation overrides of this, or null if none */
- private final String validationOverrides;
-
- private final Clock clock = Clock.fixed(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(), ZoneOffset.UTC);
-
- MockModelFactory() { this(null); }
-
- MockModelFactory(String validationOverrides) {
- this.validationOverrides = validationOverrides;
- }
-
- @Override
- public Version version() {
- return vespaVersion;
- }
-
- /** Returns the clock used by this, which is fixed at the instant 2000-01-01T00:00:00 */
- public Clock clock() { return clock; }
-
- @Override
- public Model createModel(ModelContext modelContext) {
- if (throwErrorOnLoad)
- throw new Error("Foo");
- if (throwOnLoad)
- throw new IllegalArgumentException("Foo");
- this.modelContext = modelContext;
- return loadModel();
- }
-
- Model loadModel() {
- try {
- ApplicationPackage application = new MockApplicationPackage.Builder().withEmptyHosts().withEmptyServices().withValidationOverrides(validationOverrides).build();
- DeployState deployState = new DeployState.Builder().applicationPackage(application).now(clock.instant()).build();
- return new VespaModel(deployState);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public ModelCreateResult createAndValidateModel(ModelContext modelContext, ValidationParameters validationParameters) {
- if (throwErrorOnLoad)
- throw new Error("Foo");
- if (throwOnLoad)
- throw new IllegalArgumentException("Foo");
- this.modelContext = modelContext;
- return new ModelCreateResult(loadModel(), new ArrayList<>());
- }
- }
-
-}
diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java
index b89b63aed46..d8adbd398d1 100644
--- a/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java
+++ b/configserver/src/test/java/com/yahoo/vespa/config/server/session/SessionRepositoryTest.java
@@ -2,14 +2,26 @@
package com.yahoo.vespa.config.server.session;
import com.yahoo.cloud.config.ConfigserverConfig;
+import com.yahoo.component.Version;
+import com.yahoo.config.application.api.ApplicationPackage;
+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.api.ValidationParameters;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.config.model.test.MockApplicationPackage;
import com.yahoo.config.provision.ApplicationId;
import com.yahoo.config.provision.TenantName;
import com.yahoo.text.Utf8;
import com.yahoo.vespa.config.server.ApplicationRepository;
import com.yahoo.vespa.config.server.GlobalComponentRegistry;
import com.yahoo.vespa.config.server.TestComponentRegistry;
+import com.yahoo.vespa.config.server.application.ApplicationSet;
import com.yahoo.vespa.config.server.application.OrchestratorMock;
+import com.yahoo.vespa.config.server.http.InvalidApplicationException;
import com.yahoo.vespa.config.server.http.SessionHandlerTest;
+import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry;
import com.yahoo.vespa.config.server.tenant.TenantRepository;
import com.yahoo.vespa.config.server.zookeeper.ConfigCurator;
import com.yahoo.vespa.config.util.ConfigUtils;
@@ -17,16 +29,25 @@ import com.yahoo.vespa.curator.Curator;
import com.yahoo.vespa.curator.mock.MockCurator;
import com.yahoo.vespa.flags.FlagSource;
import com.yahoo.vespa.flags.InMemoryFlagSource;
+import com.yahoo.vespa.model.VespaModel;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
+import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
import java.util.function.LongPredicate;
import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
@@ -53,9 +74,13 @@ public class SessionRepositoryTest {
}
private void setup(FlagSource flagSource) throws Exception {
+ setup(flagSource, new TestComponentRegistry.Builder());
+ }
+
+ private void setup(FlagSource flagSource, TestComponentRegistry.Builder componentRegistryBuilder) throws Exception {
curator = new MockCurator();
File configserverDbDir = temporaryFolder.newFolder().getAbsoluteFile();
- GlobalComponentRegistry globalComponentRegistry = new TestComponentRegistry.Builder()
+ GlobalComponentRegistry globalComponentRegistry = componentRegistryBuilder
.curator(curator)
.configServerConfig(new ConfigserverConfig.Builder()
.configServerDBDir(configserverDbDir.getAbsolutePath())
@@ -75,6 +100,7 @@ public class SessionRepositoryTest {
sessionRepository = tenantRepository.getTenant(tenantName).getSessionRepository();
}
+
@Test
public void require_that_local_sessions_are_created_and_deleted() throws Exception {
setup();
@@ -84,6 +110,12 @@ public class SessionRepositoryTest {
assertNotNull(sessionRepository.getLocalSession(secondSessionId));
assertNull(sessionRepository.getLocalSession(secondSessionId + 1));
+ ApplicationSet applicationSet = sessionRepository.ensureApplicationLoaded(sessionRepository.getRemoteSession(firstSessionId));
+ assertNotNull(applicationSet);
+ assertEquals(2, applicationSet.getApplicationGeneration());
+ assertEquals(applicationId.application(), applicationSet.getForVersionOrLatest(Optional.empty(), Instant.now()).getId().application());
+ assertNotNull(applicationSet.getForVersionOrLatest(Optional.empty(), Instant.now()).getModel());
+
sessionRepository.close();
// All created sessions are deleted
assertNull(sessionRepository.getLocalSession(firstSessionId));
@@ -150,6 +182,81 @@ public class SessionRepositoryTest {
assertThat(sessionRepository.getRemoteSessions().size(), is(1));
}
+ @Test(expected = InvalidApplicationException.class)
+ public void require_that_new_invalid_application_throws_exception() throws Exception {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = new Version(1, 2, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory = new MockModelFactory();
+ okFactory.vespaVersion = new Version(1, 1, 0);
+ okFactory.throwOnLoad = false;
+
+ TestComponentRegistry.Builder registryBuilder = new TestComponentRegistry.Builder()
+ .modelFactoryRegistry(new ModelFactoryRegistry(List.of(okFactory, failingFactory)));
+ setup(new InMemoryFlagSource(), registryBuilder);
+
+ deploy();
+ }
+
+ @Test
+ public void require_that_old_invalid_application_does_not_throw_exception_if_skipped_also_across_major_versions() throws Exception {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = new Version(1, 0, 0);
+ failingFactory.throwOnLoad = true;
+
+ MockModelFactory okFactory =
+ new MockModelFactory("<validation-overrides><allow until='2000-01-30'>skip-old-config-models</allow></validation-overrides>");
+ okFactory.vespaVersion = new Version(2, 0, 0);
+ okFactory.throwOnLoad = false;
+
+ TestComponentRegistry.Builder registryBuilder = new TestComponentRegistry.Builder()
+ .modelFactoryRegistry(new ModelFactoryRegistry(List.of(okFactory, failingFactory)));
+ setup(new InMemoryFlagSource(), registryBuilder);
+
+ deploy();
+ }
+
+ @Test
+ public void require_that_an_application_package_can_limit_to_one_major_version() throws Exception {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = new Version(3, 0, 0);
+ failingFactory.throwErrorOnLoad = true;
+
+ MockModelFactory okFactory = new MockModelFactory();
+ okFactory.vespaVersion = new Version(2, 0, 0);
+ okFactory.throwErrorOnLoad = false;
+
+ TestComponentRegistry.Builder registryBuilder = new TestComponentRegistry.Builder()
+ .modelFactoryRegistry(new ModelFactoryRegistry(List.of(okFactory, failingFactory)));
+ setup(new InMemoryFlagSource(), registryBuilder);
+
+ File testApp = new File("src/test/apps/app-major-version-2");
+ deploy(applicationId, testApp);
+
+ // Does not cause an error because model version 3 is skipped
+ }
+
+ @Test
+ public void require_that_an_application_package_can_limit_to_one_higher_major_version() throws Exception {
+ MockModelFactory failingFactory = new MockModelFactory();
+ failingFactory.vespaVersion = new Version(3, 0, 0);
+ failingFactory.throwErrorOnLoad = true;
+
+ MockModelFactory okFactory = new MockModelFactory();
+ okFactory.vespaVersion = new Version(1, 0, 0);
+ okFactory.throwErrorOnLoad = false;
+
+ TestComponentRegistry.Builder registryBuilder = new TestComponentRegistry.Builder()
+ .modelFactoryRegistry(new ModelFactoryRegistry(List.of(okFactory, failingFactory)));
+ setup(new InMemoryFlagSource(), registryBuilder);
+
+ File testApp = new File("src/test/apps/app-major-version-2");
+ deploy(applicationId, testApp);
+
+ // Does not cause an error because model version 3 is skipped
+ }
+
private void createSession(long sessionId, boolean wait) {
SessionZooKeeperClient zkc = new SessionZooKeeperClient(curator,
ConfigCurator.create(curator),
@@ -204,8 +311,74 @@ public class SessionRepositoryTest {
}
private long deploy(ApplicationId applicationId) {
+ return deploy(applicationId, testApp);
+ }
+
+ private long deploy(ApplicationId applicationId, File testApp) {
applicationRepository.deploy(testApp, new PrepareParams.Builder().applicationId(applicationId).build());
return applicationRepository.getActiveSession(applicationId).getSessionId();
}
+ private static class MockModelFactory implements ModelFactory {
+
+ /** Throw a RuntimeException on load - this is handled gracefully during model building */
+ boolean throwOnLoad = false;
+
+ /** Throw an Error on load - this is useful to propagate this condition all the way to the test */
+ boolean throwErrorOnLoad = false;
+
+ ModelContext modelContext;
+ public Version vespaVersion = new Version(1, 2, 3);
+
+ /** The validation overrides of this, or null if none */
+ private final String validationOverrides;
+
+ private final Clock clock = Clock.fixed(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE).atStartOfDay().atZone(ZoneOffset.UTC).toInstant(), ZoneOffset.UTC);
+
+ MockModelFactory() { this(null); }
+
+ MockModelFactory(String validationOverrides) {
+ this.validationOverrides = validationOverrides;
+ }
+
+ @Override
+ public Version version() {
+ return vespaVersion;
+ }
+
+ /** Returns the clock used by this, which is fixed at the instant 2000-01-01T00:00:00 */
+ public Clock clock() { return clock; }
+
+ @Override
+ public Model createModel(ModelContext modelContext) {
+ if (throwErrorOnLoad)
+ throw new Error("error on load");
+ if (throwOnLoad)
+ throw new IllegalArgumentException("exception on load");
+ this.modelContext = modelContext;
+ return loadModel();
+ }
+
+ Model loadModel() {
+ try {
+ ApplicationPackage application = new MockApplicationPackage.Builder().withEmptyHosts().withEmptyServices().withValidationOverrides(validationOverrides).build();
+ DeployState deployState = new DeployState.Builder().applicationPackage(application).now(clock.instant()).build();
+ return new VespaModel(deployState);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public ModelCreateResult createAndValidateModel(ModelContext modelContext, ValidationParameters validationParameters) {
+ if (throwErrorOnLoad)
+ throw new Error("error on load");
+ if (throwOnLoad)
+ throw new IllegalArgumentException("exception on load");
+ this.modelContext = modelContext;
+ return new ModelCreateResult(loadModel(), new ArrayList<>());
+ }
+ }
+
+
}