diff options
author | Harald Musum <musum@verizonmedia.com> | 2020-09-07 14:24:26 +0200 |
---|---|---|
committer | Harald Musum <musum@verizonmedia.com> | 2020-09-07 14:24:26 +0200 |
commit | d29de4a867577220a6afb838175fe73c39f1bccd (patch) | |
tree | 8275984b86f6799c6614df8a4c4ecd67ec66c5e3 /configserver | |
parent | db9105023dc06c7ca56a2914735dba663bd21d5c (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')
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<>()); + } + } + + } |