diff options
Diffstat (limited to 'configserver/src/main/java/com')
169 files changed, 15072 insertions, 0 deletions
diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java new file mode 100644 index 00000000000..bcc920614ec --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ActivateLock.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.path.Path; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.recipes.CuratorLock; + +import java.util.concurrent.TimeUnit; + +/** + * A lock to protect session activation. + * + * @author lulf + * @since 5.1 + */ +public class ActivateLock { + private static final String ACTIVATE_LOCK_NAME = "activateLock"; + private final CuratorLock curatorLock; + + public ActivateLock(Curator curator, Path rootPath) { + this.curatorLock = new CuratorLock(curator, rootPath.append(ACTIVATE_LOCK_NAME).getAbsolute()); + } + + public synchronized void acquire(TimeoutBudget timeoutBudget, boolean ignoreLockError) { + try { + curatorLock.tryLock(timeoutBudget.timeLeft().toMillis(), TimeUnit.MILLISECONDS); + } catch (Exception e) { + if (!ignoreLockError) { + throw new RuntimeException(e); + } + } + } + + public synchronized void release() { + if (curatorLock.hasLock()) { + curatorLock.unlock(); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java new file mode 100644 index 00000000000..6ff0fa49593 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationMapper.java @@ -0,0 +1,77 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Version; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.http.NotFoundException; + +/** + * Used during config request handling to route to the right config model + * based on application id and version. + * @author Vegard Sjonfjell + */ +public final class ApplicationMapper { + + private final Map<ApplicationId, ApplicationSet> requestHandlers = new ConcurrentHashMap<>(); + + private ApplicationSet getApplicationSet(ApplicationId applicationId) { + ApplicationSet list = requestHandlers.get(applicationId); + if (list != null) { + return list; + } + + throw new NotFoundException("No such application id: " + applicationId); + } + + /** + * Register a Application to an application id and specific vespa version + */ + public void register(ApplicationId applicationId, ApplicationSet applicationSet) { + requestHandlers.put(applicationId, applicationSet); + } + + /** + * Remove all applications associated with this application id + */ + public void remove(ApplicationId applicationId) { + requestHandlers.remove(applicationId); + } + + /** + * Retrieve the Application corresponding to this application id and specific vespa version. + * + * @return the matching application, or null if none matches + */ + public Application getForVersion(ApplicationId applicationId, Optional<Version> vespaVersion) throws VersionDoesNotExistException { + return getApplicationSet(applicationId).getForVersionOrLatest(vespaVersion); + } + + /** Returns whether this registry has an application for the given application id */ + public boolean hasApplication(ApplicationId applicationId) { + return hasApplicationForVersion(applicationId, Optional.<Version>empty()); + } + + /** Returns whether this registry has an application for the given application id and vespa version */ + public boolean hasApplicationForVersion(ApplicationId applicationId, Optional<Version> vespaVersion) { + try { + return getForVersion(applicationId, vespaVersion) != null; + } + catch (VersionDoesNotExistException | NotFoundException ex) { + return false; + } + } + + /** + * Get the number of applications registered + */ + public int numApplications() { + return requestHandlers.size(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java new file mode 100644 index 00000000000..dd41ef2d489 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ApplicationSet.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.application.Application; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Immutable set of {@link Application}s with the same {@link ApplicationId}. With methods for getting defaults. + * + * @author vegard + */ +public final class ApplicationSet { + + private final Version latestVersion; + // TODO: Should not need these as part of application? + private final ApplicationId applicationId; + private final long generation; + private final HashMap<Version, Application> applications = new HashMap<>(); + + private ApplicationSet(List<Application> applicationList) { + applicationId = applicationList.get(0).getId(); + generation = applicationList.get(0).getApplicationGeneration(); + for (Application application : applicationList) { + applications.put(application.getVespaVersion(), application); + if (!application.getId().equals(applicationId)) { + throw new IllegalArgumentException("Trying to create set with different application ids"); + } + } + latestVersion = applications.keySet().stream().max((a, b) -> a.compareTo(b)).get(); + } + + public Application getForVersionOrLatest(Optional<Version> optionalVersion) { + return resolveForVersion(optionalVersion.orElse(latestVersion)); + } + + private Application resolveForVersion(Version vespaVersion) { + Application application = applications.get(vespaVersion); + if (application != null) + return application; + + // Does the latest version specify we can use it regardless? + Application latest = applications.get(latestVersion); + if (latest.getModel().allowModelVersionMismatch()) + return latest; + + throw new VersionDoesNotExistException(String.format("No application with vespa version %s exists", vespaVersion.toString())); + } + + public ApplicationId getId() { + return applicationId; + } + + public static ApplicationSet fromList(List<Application> applications) { + return new ApplicationSet(applications); + } + + public static ApplicationSet fromSingle(Application application) { + return fromList(Arrays.asList(application)); + } + + public Collection<String> getAllHosts() { + return applications.values().stream() + .flatMap(app -> app.getModel().getHosts().stream() + .map(HostInfo::getHostname)) + .collect(Collectors.toList()); + } + + public void updateHostMetrics() { + for (Application application : applications.values()) { + application.updateHostMetrics(application.getModel().getHosts().size()); + } + } + + public long getApplicationGeneration() { + return generation; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java b/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java new file mode 100644 index 00000000000..b8da2448c5b --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/CompressedApplicationInputStream.java @@ -0,0 +1,128 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.common.io.ByteStreams; +import com.google.common.io.Files; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.http.BadRequestException; +import com.yahoo.vespa.config.server.http.InternalServerException; +import com.yahoo.vespa.config.server.http.SessionCreate; +import org.apache.commons.compress.archivers.ArchiveEntry; +import org.apache.commons.compress.archivers.ArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream; + +import java.io.*; +import java.util.logging.Logger; +import java.util.zip.GZIPInputStream; + +/** + * A compressed application points to an application package that can be decompressed. + * + * @author lulf + * @since 5.1 + */ +public class CompressedApplicationInputStream implements AutoCloseable { + private static final Logger log = Logger.getLogger(CompressedApplicationInputStream.class.getPackage().getName()); + private final ArchiveInputStream ais; + + /** + * Create an instance of a compressed application from an input stream. + * + * @param is the input stream containing the compressed files. + * @param contentType the content type for determining what kind of compressed stream should be used. + * @return An instance of an unpacked application. + */ + public static CompressedApplicationInputStream createFromCompressedStream(InputStream is, String contentType) { + try { + ArchiveInputStream ais = getArchiveInputStream(is, contentType); + return createFromCompressedStream(ais); + } catch (IOException e) { + throw new InternalServerException("Unable to create compressed application stream", e); + } + } + + public static CompressedApplicationInputStream createFromCompressedStream(ArchiveInputStream ais) { + return new CompressedApplicationInputStream(ais); + } + + private static ArchiveInputStream getArchiveInputStream(InputStream is, String contentTypeHeader) throws IOException { + ArchiveInputStream ais; + switch (contentTypeHeader) { + case SessionCreate.APPLICATION_X_GZIP: + ais = new TarArchiveInputStream(new GZIPInputStream(is)); + break; + case SessionCreate.APPLICATION_ZIP: + ais = new ZipArchiveInputStream(is); + break; + default: + throw new BadRequestException("Unable to decompress"); + } + return ais; + } + + private CompressedApplicationInputStream(ArchiveInputStream ais) { + this.ais = ais; + } + + /** + * Close this stream. + * @throws IOException if the stream could not be closed + */ + public void close() throws IOException { + ais.close(); + } + + File decompress() throws IOException { + return decompress(Files.createTempDir()); + } + + public File decompress(File dir) throws IOException { + decompressInto(dir); + dir = findActualApplicationDir(dir); + return dir; + } + + private void decompressInto(File application) throws IOException { + log.log(LogLevel.DEBUG, "Application is in " + application.getAbsolutePath()); + int entries = 0; + ArchiveEntry entry; + while ((entry = ais.getNextEntry()) != null) { + log.log(LogLevel.DEBUG, "Unpacking " + entry.getName()); + File outFile = new File(application, entry.getName()); + // FIXME/TODO: write more tests that break this logic. I have a feeling it is not very robust. + if (entry.isDirectory()) { + if (!(outFile.exists() && outFile.isDirectory())) { + log.log(LogLevel.DEBUG, "Creating dir: " + outFile.getAbsolutePath()); + boolean res = outFile.mkdirs(); + if (!res) { + log.log(LogLevel.WARNING, "Could not create dir " + entry.getName()); + } + } + } else { + log.log(LogLevel.DEBUG, "Creating output file: " + outFile.getAbsolutePath()); + + // Create parent dir if necessary + String parent = outFile.getParent(); + new File(parent).mkdirs(); + + FileOutputStream fos = new FileOutputStream(outFile); + ByteStreams.copy(ais, fos); + fos.close(); + } + entries++; + } + if (entries == 0) { + log.log(LogLevel.WARNING, "Not able to read any entries from " + application.getName()); + } + } + + private File findActualApplicationDir(File application) { + // If application is in e.g. application/, use that as root for UnpackedApplication + File[] files = application.listFiles(); + if (files != null && files.length == 1 && files[0].isDirectory()) { + application = files[0]; + } + return application; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java new file mode 100644 index 00000000000..3fc3c0ff8aa --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactory.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.protocol.ConfigResponse; + +/** + * Represents a component that creates config responses from a payload. Different implementations + * can do transformations of the payload such as compression. + * + * @author lulf + * @since 5.19 + */ +public interface ConfigResponseFactory { + /** + * Create a {@link ConfigResponse} for a given payload and generation. + * @param payload The {@link com.yahoo.vespa.config.ConfigPayload} to put in the response. + * @param defFile The {@link com.yahoo.config.codegen.InnerCNode} def file for this config. + * @param generation The payload generation. @return A {@link ConfigResponse} that can be sent to the client. + */ + ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java new file mode 100644 index 00000000000..309f9052a71 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigResponseFactoryFactory.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; + +/** + * Logic to select the appropriate response factory based on config. + * TODO: Move this to {@link ConfigResponseFactory} when we have java 8. + * + * @author lulf + * @since 5.20 + */ +public class ConfigResponseFactoryFactory { + public static ConfigResponseFactory createFactory(ConfigserverConfig configserverConfig) { + switch (configserverConfig.payloadCompressionType()) { + case LZ4: + return new LZ4ConfigResponseFactory(); + case UNCOMPRESSED: + return new UncompressedConfigResponseFactory(); + default: + throw new IllegalArgumentException("Unknown payload compression type " + configserverConfig.payloadCompressionType()); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java new file mode 100644 index 00000000000..b4a0f6135c3 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerBootstrap.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.inject.Inject; +import com.yahoo.component.AbstractComponent; +import com.yahoo.config.provision.Deployer; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.version.VersionState; + +/** + * Main component that bootstraps and starts config server threads. + * + * @author lulf + * @since 5.1 + */ +public class ConfigServerBootstrap extends AbstractComponent implements Runnable { + + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigServerBootstrap.class.getName()); + private final RpcServer server; + private final Thread serverThread; + + // The tenants object is injected so that all initial requests handlers are + // added to the rpcserver before it starts answering rpc requests. + @SuppressWarnings("UnusedParameters") + @Inject + public ConfigServerBootstrap(Tenants tenants, RpcServer server, Deployer deployer, VersionState versionState) { + this.server = server; + if (versionState.isUpgraded()) { + log.log(LogLevel.INFO, "Configserver upgraded from " + versionState.storedVersion() + " to " + versionState.currentVersion() + ". Redeploying all applications"); + tenants.redeployApplications(deployer); + log.log(LogLevel.INFO, "All applications redeployed"); + } + versionState.saveNewVersion(); + this.serverThread = new Thread(this, "configserver main"); + serverThread.start(); + } + + @Override + public void deconstruct() { + log.log(LogLevel.INFO, "Stopping config server"); + server.stop(); + try { + serverThread.join(); + } catch (InterruptedException e) { + log.log(LogLevel.WARNING, "Error joining server thread on shutdown: " + e.getMessage()); + } + } + + @Override + public void run() { + log.log(LogLevel.DEBUG, "Starting RPC server"); + server.run(); + log.log(LogLevel.DEBUG, "RPC server stopped"); + } + +} + diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java new file mode 100644 index 00000000000..b589e96e2e9 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerDB.java @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.application.provider.Bundle; +import com.yahoo.config.application.ConfigDefinitionDir; +import com.yahoo.io.IOUtils; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.defaults.Defaults; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Config server db is the maintainer of the serverdb directory containing def files and the file system sessions. + * See also {@link com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs} which maintains directories per tenant. + * + * @author lulf + * @since 5.1 + */ +public class ConfigServerDB { + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigServerDB.class.getName()); + private final File serverDB; + private final ConfigserverConfig configserverConfig; + + public ConfigServerDB(ConfigserverConfig configserverConfig) { + this.configserverConfig = configserverConfig; + this.serverDB = new File(Defaults.getDefaults().underVespaHome(configserverConfig.configServerDBDir())); + create(); + try { + initialize(configserverConfig.configModelPluginDir()); + } catch (IllegalArgumentException e) { + log.log(LogLevel.ERROR, "Error initializing serverdb: " + e.getMessage()); + } catch (IOException e) { + throw new RuntimeException("Unable to initialize server db", e); + } + } + + public static ConfigServerDB createTestConfigServerDb(String dir) { + return new ConfigServerDB(new ConfigserverConfig(new ConfigserverConfig.Builder().configServerDBDir(dir))); + } + + public File dest() { return new File(serverDB, "configs"); } + public File classes() { return new File(serverDB, "classes"); } + public File vespaapps() { return new File(serverDB, "vespaapps"); } + public File serverdefs() { return new File(serverDB, "serverdefs"); } + + + /** + * Creates all the config server db's dirs that are global. + */ + public void create() { + cr(dest()); + cr(classes()); + cr(vespaapps()); + cr(serverdefs()); + } + + public static void cr(File d) { + if (d.exists()) { + if (!d.isDirectory()) { + throw new IllegalArgumentException(d.getAbsolutePath() + " exists, but isn't a directory."); + } + } else { + if (!d.mkdirs()) { + throw new IllegalArgumentException("Couldn't create " + d.getAbsolutePath()); + } + } + } + + private void initialize(List<String> pluginDirectories) throws IOException { + IOUtils.recursiveDeleteDir(serverdefs()); + IOUtils.copyDirectory(classes(), serverdefs()); + ConfigDefinitionDir configDefinitionDir = new ConfigDefinitionDir(serverdefs()); + ArrayList<Bundle> bundles = new ArrayList<>(); + for (String pluginDirectory : pluginDirectories) { + bundles.addAll(Bundle.getBundles(new File(pluginDirectory))); + } + log.log(LogLevel.DEBUG, "Found " + bundles.size() + " bundles"); + List<Bundle> addedBundles = new ArrayList<>(); + for (Bundle bundle : bundles) { + log.log(LogLevel.DEBUG, "Bundle in " + bundle.getFile().getAbsolutePath() + " appears to contain " + bundle.getDefEntries().size() + " entries"); + configDefinitionDir.addConfigDefinitionsFromBundle(bundle, addedBundles); + addedBundles.add(bundle); + } + } + + public ConfigserverConfig getConfigserverConfig() { + return configserverConfig; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java new file mode 100644 index 00000000000..a84985d738c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ConfigServerSpec.java @@ -0,0 +1,73 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author tonytv + */ +public class ConfigServerSpec implements com.yahoo.config.model.api.ConfigServerSpec { + private final String hostName; + private final int configServerPort; + private final int httpPort; + private final int zooKeeperPort; + + public String getHostName() { + return hostName; + } + + public int getConfigServerPort() { + return configServerPort; + } + + public int getHttpPort() { + return httpPort; + } + + public int getZooKeeperPort() { + return zooKeeperPort; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ConfigServerSpec) { + ConfigServerSpec other = (ConfigServerSpec)o; + + return hostName.equals(other.hostName) && + configServerPort == other.configServerPort && + httpPort == other.httpPort && + zooKeeperPort == other.zooKeeperPort; + } else { + return false; + } + } + + @Override + public int hashCode() { + return hostName.hashCode(); + } + + public ConfigServerSpec(String hostName, int configServerPort, int httpPort, int zooKeeperPort) { + this.hostName = hostName; + this.configServerPort = configServerPort; + this.httpPort = httpPort; + this.zooKeeperPort = zooKeeperPort; + } + + public static List<com.yahoo.config.model.api.ConfigServerSpec> fromConfig(ConfigserverConfig configserverConfig) { + List<com.yahoo.config.model.api.ConfigServerSpec> specs = new ArrayList<>(); + for (ConfigserverConfig.Zookeeperserver server : configserverConfig.zookeeperserver()) { + // TODO We cannot be sure that http port always is rpcport + 1 + specs.add(new ConfigServerSpec(server.hostname(), configserverConfig.rpcport(), configserverConfig.rpcport() + 1, server.port())); + } + return specs; + } + + @Override + public String toString() { + return "hostname=" + hostName + ", rpc port=" + configServerPort + ", http port=" + httpPort + ", zookeeper port=" + zooKeeperPort; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java b/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java new file mode 100644 index 00000000000..1c6f75b62f0 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/DelayedConfigResponses.java @@ -0,0 +1,224 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.jrt.Target; +import com.yahoo.jrt.TargetWatcher; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.config.provision.ApplicationId; + +import java.util.*; +import java.util.concurrent.*; +import java.util.logging.Logger; + +/** + * Takes care of <i>delayed responses</i> in the config server. + * A delayed response is a response sent at request (server) timeout + * for a config which has not changed since the request was initiated. + * + * @author musum + */ +public class DelayedConfigResponses { + private static final Logger log = Logger.getLogger(DelayedConfigResponses.class.getName()); + private final RpcServer rpcServer; + + private final ScheduledExecutorService executorService; + private final boolean useJrtWatcher; + + private Map<ApplicationId, MetricUpdater> metrics = new ConcurrentHashMap<>(); + + /* Requests that resolve to config that has not changed are put on this queue. When reloading + config, all requests on this queue are reprocessed as if they were a new request */ + private final Map<ApplicationId, BlockingQueue<DelayedConfigResponse>> delayedResponses = + new ConcurrentHashMap<>(); + + public DelayedConfigResponses(RpcServer rpcServer, int numTimerThreads) { + this(rpcServer, numTimerThreads, true); + } + + // Since JRT does not allow adding watcher for "fake" requests, we must be able to disable it for unit tests :( + DelayedConfigResponses(RpcServer rpcServer, int numTimerThreads, boolean useJrtWatcher) { + this.rpcServer = rpcServer; + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(numTimerThreads, ThreadFactoryFactory.getThreadFactory(DelayedConfigResponses.class.getName())); + executor.setRemoveOnCancelPolicy(true); + this.executorService = executor; + this.useJrtWatcher = useJrtWatcher; + } + + List<DelayedConfigResponse> allDelayedResponses() { + List<DelayedConfigResponse> responses = new ArrayList<>(); + for (Map.Entry<ApplicationId, BlockingQueue<DelayedConfigResponse>> entry : delayedResponses.entrySet()) { + responses.addAll(entry.getValue()); + } + return responses; + } + + /** + * The run method of this class is run by a Timer when the timeout expires. + * The timer associated with this response must be cancelled first. + */ + public class DelayedConfigResponse implements Runnable, TargetWatcher { + + final JRTServerConfigRequest request; + private final BlockingQueue<DelayedConfigResponse> delayedResponsesQueue; + private final ApplicationId app; + private ScheduledFuture<?> future; + + public DelayedConfigResponse(JRTServerConfigRequest req, BlockingQueue<DelayedConfigResponse> delayedResponsesQueue, ApplicationId app) { + this.request = req; + this.delayedResponsesQueue = delayedResponsesQueue; + this.app = app; + } + + public synchronized void run() { + remove(); + rpcServer.addToRequestQueue(request, true, null); + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, logPre()+"DelayedConfigResponse. putting on queue: " + request.getShortDescription()); + } + } + + /** + * Remove delayed response from its queue + */ + private void remove() { + delayedResponsesQueue.remove(this); + removeWatcher(); + } + + public JRTServerConfigRequest getRequest() { + return request; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Delayed response for ").append(logPre()).append(request.getShortDescription()); + return sb.toString(); + } + + public ApplicationId getAppId() { return app; } + + String logPre() { + return Tenants.logPre(app); + } + + public synchronized boolean cancel() { + remove(); + if (future == null) { + throw new IllegalStateException("Cannot cancel a task that has not been scheduled"); + } + return future.cancel(false); + } + + public synchronized void schedule(long delay) throws InterruptedException { + delayedResponsesQueue.put(this); + future = executorService.schedule(this, delay, TimeUnit.MILLISECONDS); + addWatcher(); + } + + /** + * Removes this delayed response if target is invalid. + * + * @param target a Target that has become invalid (i.e, client has closed connection) + * @see DelayedConfigResponses + */ + @Override + public void notifyTargetInvalid(Target target) { + cancel(); + } + + private void addWatcher() { + if (useJrtWatcher) { + request.getRequest().target().addWatcher(this); + } + } + + private void removeWatcher() { + if (useJrtWatcher) { + request.getRequest().target().removeWatcher(this); + } + } + } + + /** + * Creates a DelayedConfigResponse object for taking care of requests that should + * not be responded to right away. Puts the object on the delayedResponsesQueue. + * + * NOTE: This method is called from multiple threads, so everything here needs to be + * thread safe! + * + * @param request a JRTConfigRequest + */ + public final void delayResponse(JRTServerConfigRequest request, GetConfigContext context) { + if (request.isDelayedResponse()) { + log.log(LogLevel.DEBUG, context.logPre()+"Request already delayed"); + } else { + createQueueIfNotExists(context); + BlockingQueue<DelayedConfigResponse> delayedResponsesQueue = delayedResponses.get(context.applicationId()); + DelayedConfigResponse response = new DelayedConfigResponse(request, delayedResponsesQueue, context.applicationId()); + request.setDelayedResponse(true); + try { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, context.logPre()+"Putting on delayedRequests queue (" + delayedResponsesQueue.size() + " elements): " + + response.getRequest().getShortDescription()); + } + // Config will be resolved in the run() method of DelayedConfigResponse, + // when the timer expires or config is updated/reloaded. + response.schedule(Math.max(0, request.getTimeout())); + metricDelayedResponses(context.applicationId(), delayedResponsesQueue.size()); + } catch (InterruptedException e) { + log.log(LogLevel.WARNING, context.logPre()+"Interrupted when putting on delayed requests queue."); + } + } + } + + private synchronized void metricDelayedResponses(ApplicationId app, int elems) { + if ( ! metrics.containsKey(app)) { + metrics.put(app, rpcServer.metricUpdaterFactory().getOrCreateMetricUpdater(Metrics.createDimensions(app))); + } + metrics.get(app).setDelayedResponses(elems); + } + + private synchronized void createQueueIfNotExists(GetConfigContext context) { + if ( ! delayedResponses.containsKey(context.applicationId())) { + delayedResponses.put(context.applicationId(), new LinkedBlockingQueue<>()); + } + } + + public void stop() { + executorService.shutdown(); + } + + /** + * Drains delayed responses queue and returns responses in an array + * + * @return and array of DelayedConfigResponse objects + */ + public List<DelayedConfigResponse> drainQueue(ApplicationId app) { + ArrayList<DelayedConfigResponse> ret = new ArrayList<>(); + + if (delayedResponses.containsKey(app)) { + BlockingQueue<DelayedConfigResponse> queue = delayedResponses.get(app); + queue.drainTo(ret); + } + metrics.remove(app); + return ret; + } + + public String toString() { + return "DelayedConfigResponses. Average Size=" + size(); + } + + public int size() { + int totalQueueSize = 0; + int numQueues = 0; + for (Map.Entry<ApplicationId, BlockingQueue<DelayedConfigResponse>> e : delayedResponses.entrySet()) { + numQueues++; + totalQueueSize+=e.getValue().size(); + } + return (numQueues > 0) ? (totalQueueSize / numQueues) : 0; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java new file mode 100644 index 00000000000..2c652d74b97 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/DeployHandlerLogger.java @@ -0,0 +1,48 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Slime; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A {@link DeployLogger} which persists messages as a {@link Slime} tree, and holds a tenant and application name. + * + * @author lulf + * @since 5.1 + */ +public class DeployHandlerLogger implements DeployLogger { + private static final Logger log = Logger.getLogger(DeployHandlerLogger.class.getName()); + + private final Cursor logroot; + private final boolean verbose; + private final ApplicationId app; + + public DeployHandlerLogger(Cursor root, boolean verbose, ApplicationId app) { + logroot = root; + this.verbose = verbose; + this.app = app; + } + + @Override + public void log(Level level, String message) { + if ((level == LogLevel.FINE || + level == LogLevel.DEBUG || + level == LogLevel.SPAM) && + !verbose) { + return; + } + String fullMsg = Tenants.logPre(app)+message; + Cursor entry = logroot.addObject(); + entry.setLong("time", System.currentTimeMillis()); + entry.setString("level", level.getName()); + entry.setString("message", fullMsg); + // Also tee to a normal log, Vespa log for example, but use level fine + log.log(LogLevel.FINE, fullMsg); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java new file mode 100644 index 00000000000..e27a1c56dc3 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigContext.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.vespa.config.protocol.Trace; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; + +/** + * Contains the context for serving getconfig requests so that this information does not have to be looked up multiple times. + * + * @author lulf + * @since 5.8 + */ +public class GetConfigContext { + + private final ApplicationId app; + private final RequestHandler requestHandler; + private final Trace trace; + + private GetConfigContext(ApplicationId app, RequestHandler handler, Trace trace) { + this.app = app; + this.requestHandler = handler; + this.trace = trace; + } + + public TenantName tenant() { + return app.tenant(); + } + + public ApplicationId applicationId() { + return app; + } + + public Trace trace() { + return trace; + } + + public RequestHandler requestHandler() { + return requestHandler; + } + + public static GetConfigContext create(ApplicationId app, RequestHandler handler, Trace trace) { + return new GetConfigContext(app, handler, trace); + } + + public static GetConfigContext testContext(ApplicationId app) { + return new GetConfigContext(app, null, null); + } + + /** + * Helper to produce a log preamble with the tenant and app id + * @return log msg preamble + */ + public String logPre() { + return Tenants.logPre(app); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java new file mode 100644 index 00000000000..eb5fedf9a40 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GetConfigProcessor.java @@ -0,0 +1,176 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.SentinelConfig; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.jrt.Request; +import com.yahoo.log.LogLevel; +import com.yahoo.net.LinuxInetAddress; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.ErrorCode; +import com.yahoo.vespa.config.UnknownConfigIdException; +import com.yahoo.vespa.config.protocol.*; +import com.yahoo.vespa.config.util.ConfigUtils; + +import java.net.UnknownHostException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** +* @author musum +* @since 5.1 +*/ +class GetConfigProcessor implements Runnable { + + private static final Logger log = Logger.getLogger(GetConfigProcessor.class.getName()); + private static final String localHostName; + + private final JRTServerConfigRequest request; + /* True only when this request has expired its server timeout and we need to respond to the client */ + private boolean forceResponse = false; + private final RpcServer rpcServer; + private String logPre = ""; + + GetConfigProcessor(RpcServer rpcServer, JRTServerConfigRequest request, boolean forceResponse) { + this.rpcServer = rpcServer; + this.request = request; + this.forceResponse = forceResponse; + } + + private void respond(JRTServerConfigRequest request) { + final Request req = request.getRequest(); + if (req.isError()) { + Level logLevel = (req.errorCode() == ErrorCode.APPLICATION_NOT_LOADED) ? LogLevel.DEBUG : LogLevel.INFO; + log.log(logLevel, logPre + req.errorMessage()); + } + rpcServer.respond(request); + } + + private void handleError(JRTServerConfigRequest request, int errorCode, String message) { + String target = "(unknown)"; + try { + target = request.getRequest().target().toString(); + } catch (IllegalStateException e) { + //ignore when no target + } + request.addErrorResponse(errorCode, logPre + "Failed request (" + message + ") from " + target); + respond(request); + } + + // TODO: Increment statistics (Metrics) failed counters when requests fail + public void run() { + //Request has already been detached + if (!request.validateParameters()) { + // Error code is set in verifyParameters if parameters are not OK. + log.log(LogLevel.WARNING, "Parameters for request " + request + " did not validate: " + request.errorCode() + " : " + request.errorMessage()); + respond(request); + return; + } + Trace trace = request.getRequestTrace(); + if (logDebug(trace)) { + debugLog(trace, "GetConfigProcessor.run() on " + localHostName); + } + + Optional<TenantName> tenant = rpcServer.resolveTenant(request, trace); + + // If we are certain that this request is from a node that no longer belongs to this application, + // fabricate an empty request to cause the sentinel to stop all running services + if (rpcServer.isHostedVespa() && rpcServer.allTenantsLoaded() && !tenant.isPresent() && isSentinelConfigRequest(request)) { + returnEmpty(request); + return; + } + + GetConfigContext context = rpcServer.createGetConfigContext(tenant, request, trace); + if (context == null || ! context.requestHandler().hasApplication(context.applicationId(), Optional.<Version>empty())) { + handleError(request, ErrorCode.APPLICATION_NOT_LOADED, "No application exists"); + return; + } + + Optional<Version> vespaVersion = rpcServer.useRequestVersion() ? + request.getVespaVersion().map(VespaVersion::toString).map(Version::fromString) : + Optional.empty(); + if (logDebug(trace)) { + debugLog(trace, "Using version " + getPrintableVespaVersion(vespaVersion)); + } + + if ( ! context.requestHandler().hasApplication(context.applicationId(), vespaVersion)) { + handleError(request, ErrorCode.UNKNOWN_VESPA_VERSION, "Unknown Vespa version in request: " + getPrintableVespaVersion(vespaVersion)); + return; + } + + this.logPre = Tenants.logPre(context.applicationId()); + ConfigResponse config; + try { + config = rpcServer.resolveConfig(request, context, vespaVersion); + } catch (UnknownConfigDefinitionException e) { + handleError(request, ErrorCode.UNKNOWN_DEFINITION, "Unknown config definition " + request.getConfigKey()); + return; + } catch (UnknownConfigIdException e) { + handleError(request, ErrorCode.ILLEGAL_CONFIGID, "Illegal config id " + request.getConfigKey().getConfigId()); + return; + } catch (Exception e) { + log.log(Level.SEVERE, "Unexpected error handling config request", e); + handleError(request, ErrorCode.INTERNAL_ERROR, "Internal error " + e.getMessage()); + return; + } + + // config == null is not an error, but indicates that the config will be returned later. + if ((config != null) && (!config.hasEqualConfig(request) || config.hasNewerGeneration(request) || forceResponse)) { + // debugLog(trace, "config response before encoding:" + config.toString()); + request.addOkResponse(request.payloadFromResponse(config), config.getGeneration(), config.getConfigMd5()); + if (logDebug(trace)) { + debugLog(trace, "return response: " + request.getShortDescription()); + } + respond(request); + } else { + if (logDebug(trace)) { + debugLog(trace, "delaying response " + request.getShortDescription()); + } + rpcServer.delayResponse(request, context); + } + } + + private boolean isSentinelConfigRequest(JRTServerConfigRequest request) { + return request.getConfigKey().getName().equals(SentinelConfig.getDefName()) && + request.getConfigKey().getNamespace().equals(SentinelConfig.getDefNamespace()); + } + + private static String getPrintableVespaVersion(Optional<Version> vespaVersion) { + return (vespaVersion.isPresent() ? vespaVersion.get().toString() : "LATEST"); + } + + private void returnEmpty(JRTServerConfigRequest request) { + ConfigPayload emptyPayload = ConfigPayload.empty(); + String configMd5 = ConfigUtils.getMd5(emptyPayload); + ConfigResponse config = SlimeConfigResponse.fromConfigPayload(emptyPayload, null, 0, configMd5); + request.addOkResponse(request.payloadFromResponse(config), config.getGeneration(), config.getConfigMd5()); + respond(request); + } + + /** + * Done in a static block to prevent people invoking this directly. + * Do not call java.net.Inet4AddressImpl.getLocalHostName() on each request, as this causes CPU bottlenecks. + */ + static { + String hostName = "unknown"; + try { + hostName = LinuxInetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + // ignore if it fails + } + localHostName = hostName; + } + + static boolean logDebug(Trace trace) { + return trace.shouldTrace(RpcServer.TRACELEVEL_DEBUG) || log.isLoggable(LogLevel.DEBUG); + } + + private void debugLog(Trace trace, String message) { + if (logDebug(trace)) { + log.log(LogLevel.DEBUG, logPre + message); + trace.trace(RpcServer.TRACELEVEL_DEBUG, logPre + message); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java new file mode 100644 index 00000000000..7ea65173a53 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/GlobalComponentRegistry.java @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.session.SessionPreparer; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; + +import java.util.Optional; + +/** + * Interface representing all global config server components used within the config server. + * + * @author lulf + * @since 5.1 + */ +public interface GlobalComponentRegistry { + + Curator getCurator(); + ConfigCurator getConfigCurator(); + Metrics getMetrics(); + ConfigServerDB getServerDB(); + SessionPreparer getSessionPreparer(); + ConfigserverConfig getConfigserverConfig(); + TenantListener getTenantListener(); + ReloadListener getReloadListener(); + SuperModelGenerationCounter getSuperModelGenerationCounter(); + ConfigDefinitionRepo getConfigDefinitionRepo(); + PermanentApplicationPackage getPermanentApplicationPackage(); + HostRegistries getHostRegistries(); + ModelFactoryRegistry getModelFactoryRegistry(); + Optional<Provisioner> getHostProvisioner(); + Zone getZone(); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java new file mode 100644 index 00000000000..dc411626b39 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistries.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; + +import java.util.HashMap; + +/** + * Component to hold host registries. + * + * @author musum + */ +public class HostRegistries { + + private final HostRegistry<TenantName> tenantHostRegistry = new HostRegistry<>(); + private final HashMap<TenantName, HostRegistry<ApplicationId>> applicationHostRegistries = new HashMap<>(); + + public HostRegistry<TenantName> getTenantHostRegistry() { + return tenantHostRegistry; + } + + public HostRegistry<ApplicationId> getApplicationHostRegistry(TenantName tenant) { + return applicationHostRegistries.get(tenant); + } + + public HostRegistry<ApplicationId> createApplicationHostRegistry(TenantName tenant) { + HostRegistry<ApplicationId> applicationIdHostRegistry = new HostRegistry<>(); + applicationHostRegistries.put(tenant, applicationIdHostRegistry); + return applicationIdHostRegistry; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java new file mode 100644 index 00000000000..a62e0059c2a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostRegistry.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import com.google.common.base.Predicate; +import com.google.common.collect.Collections2; +import com.yahoo.log.LogLevel; + +/** + * A host registry that create mappings between some type T and a list of hosts, represented as + * strings. + * TODO: Maybe we should have a Host type, but using String for now. + * TODO: Is there a generalized version of this pattern? Need some sort mix of Bimap and Multimap + * + * @author lulf + * @since 5.3 + */ +public class HostRegistry<T> implements HostValidator<T> { + + private static final Logger log = Logger.getLogger(HostRegistry.class.getName()); + + private final Map<T, Collection<String>> key2HostsMap = new ConcurrentHashMap<>(); + private final Map<String, T> host2KeyMap = new ConcurrentHashMap<>(); + + public T getKeyForHost(String hostName) { + return host2KeyMap.get(hostName); + } + + public void update(T key, Collection<String> newHosts) { + verifyHosts(key, newHosts); + log.log(LogLevel.DEBUG, "Setting hosts for key(" + key + "), newHosts(" + newHosts + "), currentHosts(" + getCurrentHosts(key) + ")"); + Collection<String> removedHosts = getRemovedHosts(newHosts, getCurrentHosts(key)); + removeHosts(removedHosts); + addHosts(key, newHosts); + } + + public void verifyHosts(T key, Collection<String> newHosts) { + for (String host : newHosts) { + if (hostAlreadyTaken(host, key)) { + throw new IllegalArgumentException("'" + key + "' tried to allocate host '" + host + "', but the host is already taken by '" + host2KeyMap.get(host) + "'"); + } + } + } + + public void removeHostsForKey(T key) { + for (Iterator<Map.Entry<T, Collection<String>>> it = key2HostsMap.entrySet().iterator(); it.hasNext(); ) { + Map.Entry<T, Collection<String>> entry = it.next(); + if (entry.getKey().equals(key)) { + Collection<String> hosts = entry.getValue(); + it.remove(); + removeHosts(hosts); + } + } + } + + public Collection<String> getAllHosts() { + return Collections.unmodifiableCollection(new ArrayList<>(host2KeyMap.keySet())); + } + + Collection<String> getCurrentHosts(T key) { + return key2HostsMap.containsKey(key) ? new ArrayList<>(key2HostsMap.get(key)) : new ArrayList<String>(); + } + + private boolean hostAlreadyTaken(String host, T key) { + return host2KeyMap.containsKey(host) && !key.equals(host2KeyMap.get(host)); + } + + private static Collection<String> getRemovedHosts(final Collection<String> newHosts, Collection<String> previousHosts) { + return Collections2.filter(previousHosts, new Predicate<String>() { + @Override + public boolean apply(String host) { + return !newHosts.contains(host); + } + }); + } + + private void removeHosts(Collection<String> removedHosts) { + for (String host : removedHosts) { + log.log(LogLevel.DEBUG, "Removing " + host); + host2KeyMap.remove(host); + } + } + + private void addHosts(T key, Collection<String> newHosts) { + for (String host : newHosts) { + log.log(LogLevel.DEBUG, "Adding " + host); + host2KeyMap.put(host, key); + } + key2HostsMap.put(key, new ArrayList<>(newHosts)); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java new file mode 100644 index 00000000000..67292e281bb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/HostValidator.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.Collection; + +/** + * A read only host registry that has mappings from a host to some type T. + * strings. + * + * @author lulf + * @since 5.9 + */ +public interface HostValidator<T> { + + void verifyHosts(T key, Collection<String> newHosts); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java new file mode 100644 index 00000000000..fd5529cdffd --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/InjectedGlobalComponentRegistry.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.SessionPreparer; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; + +import java.util.Optional; + +/** + * Registry containing all the "static"/"global" components in a config server in one place. + * + * @author lulf + * @since 5.1 + */ +public class InjectedGlobalComponentRegistry implements GlobalComponentRegistry { + + private final Curator curator; + private final ConfigCurator configCurator; + private final Metrics metrics; + private final ModelFactoryRegistry modelFactoryRegistry; + private final ConfigServerDB serverDB; + private final SessionPreparer sessionPreparer; + private final RpcServer rpcServer; + private final ConfigserverConfig configserverConfig; + private final SuperModelGenerationCounter superModelGenerationCounter; + private final ConfigDefinitionRepo defRepo; + private final PermanentApplicationPackage permanentApplicationPackage; + private final HostRegistries hostRegistries; + private final Optional<Provisioner> hostProvisioner; + private final Zone zone; + + @Inject + public InjectedGlobalComponentRegistry(Curator curator, + ConfigCurator configCurator, + Metrics metrics, + ModelFactoryRegistry modelFactoryRegistry, + ConfigServerDB serverDB, + SessionPreparer sessionPreparer, + RpcServer rpcServer, + ConfigserverConfig configserverConfig, + SuperModelGenerationCounter superModelGenerationCounter, + ConfigDefinitionRepo defRepo, + PermanentApplicationPackage permanentApplicationPackage, + HostRegistries hostRegistries, + HostProvisionerProvider hostProvisionerProvider, + Zone zone) { + this.curator = curator; + this.configCurator = configCurator; + this.metrics = metrics; + this.modelFactoryRegistry = modelFactoryRegistry; + this.serverDB = serverDB; + this.sessionPreparer = sessionPreparer; + this.rpcServer = rpcServer; + this.configserverConfig = configserverConfig; + this.superModelGenerationCounter = superModelGenerationCounter; + this.defRepo = defRepo; + this.permanentApplicationPackage = permanentApplicationPackage; + this.hostRegistries = hostRegistries; + this.hostProvisioner = hostProvisionerProvider.getHostProvisioner(); + this.zone = zone; + } + + @Override + public Curator getCurator() { return curator; } + @Override + public ConfigCurator getConfigCurator() { return configCurator; } + @Override + public Metrics getMetrics() { return metrics; } + @Override + public ConfigServerDB getServerDB() { return serverDB; } + @Override + public SessionPreparer getSessionPreparer() { return sessionPreparer; } + @Override + public ConfigserverConfig getConfigserverConfig() { return configserverConfig; } + @Override + public TenantListener getTenantListener() { return rpcServer; } + @Override + public ReloadListener getReloadListener() { return rpcServer; } + @Override + public SuperModelGenerationCounter getSuperModelGenerationCounter() { return superModelGenerationCounter; } + @Override + public ConfigDefinitionRepo getConfigDefinitionRepo() { return defRepo; } + @Override + public PermanentApplicationPackage getPermanentApplicationPackage() { return permanentApplicationPackage; } + @Override + public HostRegistries getHostRegistries() { return hostRegistries; } + @Override + public ModelFactoryRegistry getModelFactoryRegistry() { return modelFactoryRegistry; } + + @Override + public Optional<Provisioner> getHostProvisioner() { + return hostProvisioner; + } + + @Override + public Zone getZone() { + return zone; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java new file mode 100644 index 00000000000..66a2dc32eea --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/LZ4ConfigResponseFactory.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.text.Utf8Array; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.LZ4PayloadCompressor; +import com.yahoo.vespa.config.protocol.CompressionInfo; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.SlimeConfigResponse; +import com.yahoo.vespa.config.util.ConfigUtils; + +/** + * Compressor that compresses config payloads to lz4. + * + * @author lulf + * @since 5.19 + */ +public class LZ4ConfigResponseFactory implements ConfigResponseFactory { + + private static LZ4PayloadCompressor compressor = new LZ4PayloadCompressor(); + + @Override + public ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation) { + Utf8Array rawPayload = payload.toUtf8Array(true); + String configMd5 = ConfigUtils.getMd5(rawPayload); + CompressionInfo info = CompressionInfo.create(CompressionType.LZ4, rawPayload.getByteLength()); + Utf8Array compressed = new Utf8Array(compressor.compress(rawPayload.getBytes())); + return new SlimeConfigResponse(compressed, defFile, generation, configMd5, info); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java new file mode 100644 index 00000000000..ca715d66a05 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/PathProvider.java @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.inject.Inject; +import com.yahoo.path.Path; + +/** + * Temporary provider of root path for components that will soon get them injected from a parent class. + * + * @author lulf + * * @since 5.1.24 + */ +public class PathProvider { + private final Path root; + // Path for Vespa-related data stored in Zookeeper (subpaths are relative to this path) + // NOTE: This should not be exposed, as this path can be different in testing, depending on how we configure it. + private static final String APPS_ZK_NODE = "sessions"; + private static final String VESPA_ZK_PATH = "/vespa/config"; + //private static final String VESPA_ZK_PATH = "/config/v2/tenants/default"; + private static final String LIVEAPP_ZK_NODE = "liveapp"; + + @Inject + public PathProvider() { + root = Path.fromString(VESPA_ZK_PATH); + } + + public PathProvider(Path root) { + this.root = root; + } + + public Path getRoot() { + return root; + } + + public Path getSessionDirs() { + return root.append(APPS_ZK_NODE); + } + + public Path getSessionDir(long sessionId) { + return getSessionDirs().append(String.valueOf(sessionId)); + } + + public Path getLiveApp() { + return root.append(LIVEAPP_ZK_NODE); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java new file mode 100644 index 00000000000..8d7eb8a9d7c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadHandler.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; + +/** + * Interface representing a reload handler. + * + * @author lulf + * @since 5.1.24 + */ +public interface ReloadHandler { + /** + * Reload config with the one contained in the application. + * + * @param applicationSet The set of applications to set as active. + */ + public void reloadConfig(ApplicationSet applicationSet); + + /** + * Remove an application and resources related to it. + * + * @param applicationId to be removed + */ + public void removeApplication(ApplicationId applicationId); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java new file mode 100644 index 00000000000..f519a656c8f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ReloadListener.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; + +import java.util.Collection; + +/** + * A ReloadListener is used to signal to a component that config has been + * reloaded. It only exists because the RpcServer cannot distinguish between a + * successful reload of a new application and a reload of the same application. + * + * @author lulf + * @since 5.1 + */ +public interface ReloadListener { + + /** + * Signal the listener that config has been reloaded. + * + * @param tenant Name of tenant for which config was reloaded. + * @param application the {@link com.yahoo.vespa.config.server.application.Application} that will be reloaded + */ + public void configReloaded(TenantName tenant, ApplicationSet application); + + /** + * Signal the listener that hosts used by by a particular tenant. + * + * @param tenant Name of tenant. + * @param newHosts a {@link Collection} of hosts used by tenant. + */ + void hostsUpdated(TenantName tenant, Collection<String> newHosts); + + /** + * Verify that given hosts are available for use by tenant. + * TODO: Does not belong here... + * + * @param tenant tenant that wants to allocate hosts. + * @param newHosts a {@link java.util.Collection} of hosts that tenant wants to allocate. + * @throws java.lang.IllegalArgumentException if one or more of the hosts are in use by another tenant. + */ + void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts); + + /** + * Notifies listener that application with id {@link ApplicationId} has been removed. + * + * @param applicationId The {@link ApplicationId} of the removed application. + */ + void applicationRemoved(ApplicationId applicationId); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java new file mode 100644 index 00000000000..691035839bb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RequestHandler.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.Optional; +import java.util.Set; + +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.protocol.ConfigResponse; + +/** + * Instances of this can serve misc config related requests + * + * @author lulf + * @since 5.1 + */ +public interface RequestHandler { + + /** + * Resolves a config. Mandatory subclass hook for getConfig(). + * @param appId The application id to use + * @param req a config request + * @param vespaVersion vespa version + * @return The resolved config if it exists, else null. + */ + public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion); + + /** + * Lists all configs (name, configKey) in the config model. + * @param appId application id to use + * @param vespaVersion optional vespa version + * @param recursive If true descend into all levels + * @return set of keys + */ + public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive); + + /** + * Lists all configs (name, configKey) of the given key. The config id of the key is interpreted as a prefix to match. + * @param appId application id to use + * @param vespaVersion optional vespa version + * @param key def key to match + * @param recursive If true descend into all levels + * @return set of keys + */ + public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive); + + /** + * Lists all available configs produced + * @param appId application id to use + * @param vespaVersion optional vespa version + * @return set of keys + */ + public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion); + + /** + * List all config ids present + * @param appId application id to use + * @param vespaVersion optional vespa version + * @return a Set containing all config ids available + */ + public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion); + + /** + * True if application loaded + * @param appId The application id to use + * @param vespaVersion optional vespa version + * @return true if app loaded + */ + boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion); + + /** + * Resolve {@link ApplicationId} for a given hostname. Returns a default {@link ApplicationId} if no applications + * are found for that host. + * + * @param hostName hostname of client. + * @return an {@link ApplicationId} instance. + */ + ApplicationId resolveApplicationId(String hostName); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java new file mode 100644 index 00000000000..2a686e2dee3 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RotationsCache.java @@ -0,0 +1,69 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.path.Path; + +import com.yahoo.vespa.curator.Curator; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Rotations for an application. Persisted in ZooKeeper. + * + * @author musum + */ +public class RotationsCache { + + private final Path path; + private final Curator curator; + + public RotationsCache(Curator curator, Path tenantPath) { + this.curator = curator; + this.path = tenantPath.append("rotationsCache/"); + } + + public Set<Rotation> readRotationsFromZooKeeper(ApplicationId applicationId) { + ObjectMapper objectMapper = new ObjectMapper(); + Path fullPath = path.append(applicationId.serializedForm()); + Set<Rotation> ret = new LinkedHashSet<>(); + try { + if (curator != null && curator.exists(fullPath)) { + byte[] data = curator.framework().getData().forPath(fullPath.getAbsolute()); + if (data.length > 0) { + Set<String> rotationIds = objectMapper.readValue(data, new TypeReference<Set<String>>() { + }); + ret.addAll(rotationIds.stream().map(Rotation::new).collect(Collectors.toSet())); + } + } + } catch (Exception e) { + throw new RuntimeException("Error reading rotations from ZooKeeper (" + fullPath + ")", e); + } + return ret; + } + + public void writeRotationsToZooKeeper(ApplicationId applicationId, Set<Rotation> rotations) { + if (rotations.size() > 0) { + final ObjectMapper objectMapper = new ObjectMapper(); + final Path cachePath = path.append(applicationId.serializedForm()); + final String absolutePath = cachePath.getAbsolute(); + try { + curator.create(cachePath); + final Set<String> rotationIds = rotations.stream().map(Rotation::getId).collect(Collectors.toSet()); + final byte[] data = objectMapper.writeValueAsBytes(rotationIds); + curator.framework().setData().forPath(absolutePath, data); + } catch (Exception e) { + throw new RuntimeException("Error writing rotations to ZooKeeper (" + absolutePath + ")", e); + } + } + } + + public void deleteRotationFromZooKeeper(ApplicationId applicationId) { + curator.delete(path.append(applicationId.serializedForm())); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java new file mode 100644 index 00000000000..f49268dd800 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/RpcServer.java @@ -0,0 +1,407 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.jrt.Acceptor; +import com.yahoo.jrt.Int32Value; +import com.yahoo.jrt.ListenFailedException; +import com.yahoo.jrt.Method; +import com.yahoo.jrt.Request; +import com.yahoo.jrt.Spec; +import com.yahoo.jrt.StringValue; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Transport; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.ErrorCode; +import com.yahoo.vespa.config.JRTMethods; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.JRTConfigRequest; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequest; +import com.yahoo.vespa.config.protocol.JRTServerConfigRequestV3; +import com.yahoo.vespa.config.protocol.Trace; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.MetricUpdaterFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * An RPC server class that handles the config protocol RPC method "getConfigV3". + * Mandatory hooks need to be implemented by subclasses. + * + * @author <a href="musum@yahoo-inc.com">Harald Musum</a> + */ +public class RpcServer implements Runnable, ReloadListener, TenantListener { + + static final int TRACELEVEL = 6; + static final int TRACELEVEL_DEBUG = 9; + private static final String THREADPOOL_NAME = "rpcserver worker pool"; + private static final long SHUTDOWN_TIMEOUT = 60; + private final Supervisor supervisor = new Supervisor(new Transport()); + private Spec spec = null; + private boolean running = false; + private final boolean useRequestVersion; + private final boolean hostedVespa; + + private static final Logger log = Logger.getLogger(RpcServer.class.getName()); + + final DelayedConfigResponses delayedConfigResponses; + + private final HostRegistry<TenantName> hostRegistry; + private final Map<TenantName, TenantHandlerProvider> tenantProviders = new ConcurrentHashMap<>(); + private final SuperModelController superModelController; + private final MetricUpdater metrics; + private final MetricUpdaterFactory metricUpdaterFactory; + + private final ThreadPoolExecutor executorService; + private volatile boolean allTenantsLoaded = false; + + /** + * Creates an RpcServer listening on the specified <code>port</code>. + * + * @param config The config to use for setting up this server + */ + @Inject + public RpcServer(ConfigserverConfig config, SuperModelController superModelController, MetricUpdaterFactory metrics, HostRegistries hostRegistries) { + this.superModelController = superModelController; + this.metricUpdaterFactory = metrics; + this.supervisor.setMaxOutputBufferSize(config.maxoutputbuffersize()); + this.metrics = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap()); + BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(config.maxgetconfigclients()); + executorService = new ThreadPoolExecutor(config.numthreads(), config.numthreads(), 0, TimeUnit.SECONDS, workQueue, ThreadFactoryFactory.getThreadFactory(THREADPOOL_NAME)); + delayedConfigResponses = new DelayedConfigResponses(this, config.numDelayedResponseThreads()); + spec = new Spec(null, config.rpcport()); + hostRegistry = hostRegistries.getTenantHostRegistry(); + this.useRequestVersion = config.useVespaVersionInRequest(); + this.hostedVespa = config.hostedVespa(); + setUpHandlers(); + } + + /** + * Handles RPC method "config.v3.getConfig" requests. + * Uses the template pattern to call methods in classes that extend RpcServer. + * + * @param req a Request + */ + @SuppressWarnings({"UnusedDeclaration"}) + public final void getConfigV3(Request req) { + if (log.isLoggable(LogLevel.SPAM)) { + log.log(LogLevel.SPAM, "getConfigV3"); + } + req.detach(); + JRTServerConfigRequestV3 request = JRTServerConfigRequestV3.createFromRequest(req); + addToRequestQueue(request); + } + + /** + * Returns 0 if server is alive. + * + * @param req a Request + */ + @SuppressWarnings("UnusedDeclaration") + public final void ping(Request req) { + req.returnValues().add(new Int32Value(0)); + } + + /** + * Returns a String with statistics data for the server. + * + * @param req a Request + */ + public final void printStatistics(Request req) { + req.returnValues().add(new StringValue("Delayed responses queue size: " + delayedConfigResponses.size())); + } + + public void run() { + log.log(LogLevel.DEBUG, "Ready for requests on " + spec); + try { + Acceptor acceptor = supervisor.listen(spec); + running = true; + supervisor.transport().join(); + acceptor.shutdown().join(); + } catch (ListenFailedException e) { + stop(); + throw new RuntimeException("Could not listen at " + spec, e); + } + running = false; + } + + public void stop() { + executorService.shutdown(); + try { + executorService.awaitTermination(SHUTDOWN_TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.interrupted(); // Ignore and continue shutdown. + } + delayedConfigResponses.stop(); + supervisor.transport().shutdown().join(); + } + + /** + * Set up RPC method handlers. + */ + private void setUpHandlers() { + // The getConfig method in this class will handle RPC calls for getting config + getSupervisor().addMethod(JRTMethods.createConfigV3GetConfigMethod(this, "getConfigV3")); + getSupervisor().addMethod(new Method("ping", "", "i", + this, "ping") + .methodDesc("ping") + .returnDesc(0, "ret code", "return code, 0 is OK")); + getSupervisor().addMethod(new Method("printStatistics", "", "s", + this, "printStatistics") + .methodDesc("printStatistics") + .returnDesc(0, "statistics", "Statistics for server")); + } + + public boolean isRunning() { + return running; + } + + /** + * Checks all delayed responses for config changes and waits until all has been answered. + * This method should be called when config is reloaded in the server. + */ + @Override + public void configReloaded(TenantName tenant, ApplicationSet applicationSet) { + final ApplicationId applicationId = applicationSet.getId(); + configReloaded(delayedConfigResponses.drainQueue(applicationId), Tenants.logPre(applicationId)); + reloadSuperModel(tenant, applicationSet); + } + + private void reloadSuperModel(TenantName tenant, ApplicationSet applicationSet) { + superModelController.reloadConfig(tenant, applicationSet); + configReloaded(delayedConfigResponses.drainQueue(ApplicationId.global()), Tenants.logPre(ApplicationId.global())); + } + + private void configReloaded(List<DelayedConfigResponses.DelayedConfigResponse> responses, String logPre) { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, logPre + "Start of configReload: " + responses.size() + " requests on delayed requests queue"); + } + int responsesSent = 0; + CompletionService<Boolean> completionService = new ExecutorCompletionService<>(executorService); + while (!responses.isEmpty()) { + DelayedConfigResponses.DelayedConfigResponse delayedConfigResponse = responses.remove(0); + // Discard the ones that we have already answered + // Doing cancel here deals with the case where the timer is already running or has not run, so + // there is no need for any extra check. + if (delayedConfigResponse.cancel()) { + if (log.isLoggable(LogLevel.DEBUG)) { + logRequestDebug(LogLevel.DEBUG, logPre + "Timer cancelled for ", delayedConfigResponse.request); + } + // Do not wait for this request if we were unable to execute + if (addToRequestQueue(delayedConfigResponse.request, false, completionService)) { + responsesSent++; + } + } else { + log.log(LogLevel.DEBUG, logPre + "Timer already cancelled or finished or never scheduled"); + } + } + + for (int i = 0; i < responsesSent; i++) { + + try { + completionService.take(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + log.log(LogLevel.DEBUG, logPre + "Finished reloading " + responsesSent + " requests"); + } + + private void logRequestDebug(LogLevel level, String message, JRTServerConfigRequest request) { + if (log.isLoggable(level)) { + log.log(level, message + request.getShortDescription()); + } + } + + @Override + public void hostsUpdated(TenantName tenant, Collection<String> newHosts) { + log.log(LogLevel.DEBUG, "Updating hosts in tenant host registry '" + hostRegistry + "' with " + newHosts); + hostRegistry.update(tenant, newHosts); + } + + @Override + public void verifyHostsAreAvailable(TenantName tenant, Collection<String> newHosts) { + hostRegistry.verifyHosts(tenant, newHosts); + } + + @Override + public void applicationRemoved(ApplicationId applicationId) { + superModelController.removeApplication(applicationId); + configReloaded(delayedConfigResponses.drainQueue(applicationId), Tenants.logPre(applicationId)); + configReloaded(delayedConfigResponses.drainQueue(ApplicationId.global()), Tenants.logPre(ApplicationId.global())); + } + + public Spec getSpec() { + return spec; + } + + public void respond(JRTServerConfigRequest request) { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Trace at request return:\n" + request.getRequestTrace().toString()); + } + request.getRequest().returnRequest(); + } + + /** + * Returns the tenant for this request, empty if there is no tenant for this request + * (which on hosted Vespa means that the requesting host is not currently active for any tenant) + */ + public Optional<TenantName> resolveTenant(JRTServerConfigRequest request, Trace trace) { + if ("*".equals(request.getConfigKey().getConfigId())) return Optional.of(ApplicationId.global().tenant()); + String hostname = request.getClientHostName(); + TenantName tenant = hostRegistry.getKeyForHost(hostname); + if (tenant == null) { + if (GetConfigProcessor.logDebug(trace)) { + String message = "Did not find tenant for host '" + hostname + "', using " + TenantName.defaultName(); + log.log(LogLevel.DEBUG, message); + log.log(LogLevel.DEBUG, "hosts in host registry: " + hostRegistry.getAllHosts()); + trace.trace(6, message); + } + return Optional.empty(); + } + return Optional.of(tenant); + } + + public ConfigResponse resolveConfig(JRTServerConfigRequest request, GetConfigContext context, Optional<Version> vespaVersion) { + Trace trace = context.trace(); + if (trace.shouldTrace(TRACELEVEL)) { + trace.trace(TRACELEVEL, "RpcServer.resolveConfig()"); + } + RequestHandler handler = context.requestHandler(); + return handler.resolveConfig(context.applicationId(), request, vespaVersion); + + } + + protected Supervisor getSupervisor() { + return supervisor; + } + + Boolean addToRequestQueue(JRTServerConfigRequest request) { + return addToRequestQueue(request, false, null); + } + + public Boolean addToRequestQueue(JRTServerConfigRequest request, boolean forceResponse, CompletionService<Boolean> completionService) { + // It's no longer delayed if we get here + request.setDelayedResponse(false); + //ConfigDebug.logDebug(log, System.currentTimeMillis(), request.getConfigKey(), "RpcServer.addToRequestQueue()"); + try { + final GetConfigProcessor task = new GetConfigProcessor(this, request, forceResponse); + if (completionService == null) { + executorService.submit(task); + } else { + completionService.submit(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + task.run(); + return true; + } + }); + } + updateWorkQueueMetrics(); + return true; + } catch (RejectedExecutionException e) { + request.addErrorResponse(ErrorCode.INTERNAL_ERROR, "getConfig request queue size is larger than configured max limit"); + respond(request); + return false; + } + } + + private void updateWorkQueueMetrics() { + int queued = executorService.getQueue().size(); + metrics.setRpcServerQueueSize(queued); + } + + /** + * Returns the context for this request, or null if the server is not properly set up with handlers + */ + public GetConfigContext createGetConfigContext(Optional<TenantName> optionalTenant, JRTServerConfigRequest request, Trace trace) { + if ("*".equals(request.getConfigKey().getConfigId())) { + return GetConfigContext.create(ApplicationId.global(), superModelController, trace); + } + TenantName tenant = optionalTenant.orElse(TenantName.defaultName()); // perhaps needed for non-hosted? + if ( ! hasRequestHandler(tenant)) { + String msg = Tenants.logPre(tenant) + "Unable to find request handler for tenant. Requested from host '" + request.getClientHostName() + "'"; + metrics.incUnknownHostRequests(); + trace.trace(TRACELEVEL, msg); + log.log(LogLevel.WARNING, msg); + return null; + } + RequestHandler handler = getRequestHandler(tenant); + ApplicationId applicationId = handler.resolveApplicationId(request.getClientHostName()); + if (trace.shouldTrace(TRACELEVEL_DEBUG)) { + trace.trace(TRACELEVEL_DEBUG, "Host '" + request.getClientHostName() + "' should have config from application '" + applicationId + "'"); + } + return GetConfigContext.create(applicationId, handler, trace); + } + + private boolean hasRequestHandler(TenantName tenant) { + return tenantProviders.containsKey(tenant); + } + + private RequestHandler getRequestHandler(TenantName tenant) { + if (!tenantProviders.containsKey(tenant)) { + throw new IllegalStateException("No request handler for " + tenant); + } + return tenantProviders.get(tenant).getRequestHandler(); + } + + public void delayResponse(JRTServerConfigRequest request, GetConfigContext context) { + delayedConfigResponses.delayResponse(request, context); + } + + @Override + public void onTenantDelete(TenantName tenant) { + log.log(LogLevel.DEBUG, Tenants.logPre(tenant)+"Tenant deleted, removing request handler and cleaning host registry"); + if (tenantProviders.containsKey(tenant)) { + tenantProviders.remove(tenant); + } + hostRegistry.removeHostsForKey(tenant); + } + + @Override + public void onTenantsLoaded() { + allTenantsLoaded = true; + superModelController.enable(); + } + + @Override + public void onTenantCreate(TenantName tenant, TenantHandlerProvider tenantHandlerProvider) { + log.log(LogLevel.DEBUG, Tenants.logPre(tenant)+"Tenant created, adding request handler"); + tenantProviders.put(tenant, tenantHandlerProvider); + } + + /** Returns true only after all tenants are loaded */ + public boolean allTenantsLoaded() { return allTenantsLoaded; } + + /** Returns true if this rpc server is currently running in a hosted Vespa configuration */ + public boolean isHostedVespa() { return hostedVespa; } + + MetricUpdaterFactory metricUpdaterFactory() { + return metricUpdaterFactory; + } + + boolean useRequestVersion() { + return useRequestVersion; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java b/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java new file mode 100644 index 00000000000..8858d368edf --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/ServerCache.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.vespa.config.ConfigCacheKey; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.vespa.config.protocol.ConfigResponse; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Cache that holds configs and config definitions. It has separate maps for the separate + * "types", for clarity. + * + * @author vegardh + */ +public class ServerCache { + + private final Map<ConfigDefinitionKey, ConfigDefinition> defs = new ConcurrentHashMap<>(); + /* Legacy user configs from configs/ dir in application package (NB! Only name, not key) */ + private final Map<String, ConfigPayload> legacyUserCfgs = new ConcurrentHashMap<>(); + + // NOTE: The reason we do a double mapping here is to dedup configs that have the same md5. + private final Map<ConfigCacheKey, String> md5Sums = new ConcurrentHashMap<>(); + private final Map<String, ConfigResponse> md5ToConfig = new ConcurrentHashMap<>(); + + public void addDef(ConfigDefinitionKey key, ConfigDefinition def) { + defs.put(key, def); + } + + public void addLegacyUserConfig(String name, ConfigPayload config) { + legacyUserCfgs.put(name, config); + } + + public ConfigPayload getLegacyUserConfig(String name) { + return legacyUserCfgs.get(name); + } + + public void put(ConfigCacheKey key, ConfigResponse config, String configMd5) { + md5Sums.put(key, configMd5); + md5ToConfig.put(configMd5, config); + } + + public ConfigResponse get(ConfigCacheKey key) { + String md5 = md5Sums.get(key); + if (md5 == null) return null; + return md5ToConfig.get(md5); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Cache\n"); + sb.append("defs: ").append(defs.size()).append("\n"); + sb.append("user cfgs: ").append(legacyUserCfgs.size()).append("\n"); + sb.append("md5sums: ").append(md5Sums.size()).append("\n"); + sb.append("md5ToConfig: ").append(md5ToConfig.size()).append("\n"); + + return sb.toString(); + } + + public ConfigDefinition getDef(ConfigDefinitionKey defKey) { + return defs.get(defKey); + } + + /** + * The number of different {@link ConfigResponse} elements + * @return elems + */ + public int configElems() { + return md5ToConfig.size(); + } + + /** + * The number of different key→checksum mappings + * @return elems + */ + public int checkSumElems() { + return md5Sums.size(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java new file mode 100644 index 00000000000..b3921da869a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/StaticConfigDefinitionRepo.java @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.inject.Inject; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; + +import java.util.Collections; +import java.util.Map; + +/** + * A global pool of all config definitions that this server knows about. These objects can be shared + * by all tenants, as they are not modified. + * + * @author lulf + * @since 5.10 + */ +public class StaticConfigDefinitionRepo implements ConfigDefinitionRepo { + + private final ConfigDefinitionRepo repo; + + // Only useful in tests that dont need full blown repo. + public StaticConfigDefinitionRepo() { + this.repo = new ConfigDefinitionRepo() { + @Override + public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() { + return Collections.emptyMap(); + } + }; + } + + @Inject + public StaticConfigDefinitionRepo(ConfigServerDB serverDB) { + this.repo = new com.yahoo.config.model.application.provider.StaticConfigDefinitionRepo(serverDB.serverdefs()); + } + + @Override + public Map<ConfigDefinitionKey, ConfigDefinition> getConfigDefinitions() { + return repo.getConfigDefinitions(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java new file mode 100644 index 00000000000..f6b00440e30 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelController.java @@ -0,0 +1,161 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.provision.Version; +import com.yahoo.config.provision.Zone; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.GenerationCounter; +import com.yahoo.vespa.config.server.model.SuperModel; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import com.yahoo.cloud.config.ElkConfig; + +/** + * Controls the lifetime of the {@link SuperModel} and the {@link SuperModelRequestHandler}. + * + * @author lulf + * @since 5.9 + */ +public class SuperModelController implements RequestHandler { + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SuperModelController.class.getName()); + private volatile SuperModelRequestHandler handler; + private final GenerationCounter generationCounter; + private final Zone zone; + private final long masterGeneration; + private final ConfigDefinitionRepo configDefinitionRepo; + private final ConfigResponseFactory responseFactory; + private final ElkConfig elkConfig; + private volatile boolean enabled = false; + + + public SuperModelController(GenerationCounter generationCounter, ConfigDefinitionRepo configDefinitionRepo, ConfigserverConfig configserverConfig, ElkConfig elkConfig) { + this.generationCounter = generationCounter; + this.configDefinitionRepo = configDefinitionRepo; + this.elkConfig = elkConfig; + this.masterGeneration = configserverConfig.masterGeneration(); + this.responseFactory = ConfigResponseFactoryFactory.createFactory(configserverConfig); + this.zone = new Zone(configserverConfig); + this.handler = createNewHandler(Collections.emptyMap()); + } + + /** + * Signals that config has been reloaded for an {@link com.yahoo.vespa.config.server.application.Application} + * belonging to a tenant. + * + * TODO: This is a bit too complex I think. + * + * @param tenant Name of tenant owning the application. + * @param applicationSet The reloaded set of {@link com.yahoo.vespa.config.server.application.Application}. + */ + public synchronized void reloadConfig(TenantName tenant, ApplicationSet applicationSet) { + Map<TenantName, Map<ApplicationId, Application>> newModels = createModelCopy(); + if (!newModels.containsKey(tenant)) { + newModels.put(tenant, new LinkedHashMap<>()); + } + // TODO: Should supermodel care about multiple versions? + newModels.get(tenant).put(applicationSet.getId(), applicationSet.getForVersionOrLatest(Optional.empty())); + handler = createNewHandler(newModels); + } + + public synchronized void removeApplication(ApplicationId applicationId) { + Map<TenantName, Map<ApplicationId, Application>> newModels = createModelCopy(); + if (newModels.containsKey(applicationId.tenant())) { + newModels.get(applicationId.tenant()).remove(applicationId); + if (newModels.get(applicationId.tenant()).isEmpty()) { + newModels.remove(applicationId.tenant()); + } + } + handler = createNewHandler(newModels); + } + + private SuperModelRequestHandler createNewHandler(Map<TenantName, Map<ApplicationId, Application>> newModels) { + long generation = generationCounter.get() + masterGeneration; + SuperModel model = new SuperModel(newModels, elkConfig, zone); + return new SuperModelRequestHandler(model, configDefinitionRepo, generation, responseFactory); + } + + private Map<TenantName, Map<ApplicationId, Application>> getCurrentModels() { + if (handler != null) { + return handler.getSuperModel().getCurrentModels(); + } else { + return new LinkedHashMap<>(); + } + } + + private Map<TenantName, Map<ApplicationId, Application>> createModelCopy() { + Map<TenantName, Map<ApplicationId, Application>> currentModels = getCurrentModels(); + Map<TenantName, Map<ApplicationId, Application>> newModels = new LinkedHashMap<>(); + for (Map.Entry<TenantName, Map<ApplicationId, Application>> entry : currentModels.entrySet()) { + Map<ApplicationId, Application> appMap = new LinkedHashMap<>(); + newModels.put(entry.getKey(), appMap); + for (Map.Entry<ApplicationId, Application> appEntry : entry.getValue().entrySet()) { + appMap.put(appEntry.getKey(), appEntry.getValue()); + } + } + return newModels; + } + + public SuperModelRequestHandler getHandler() { return handler; } + + @Override + public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) { + log.log(LogLevel.DEBUG, "SuperModelController resolving " + req + " for app id '" + appId + "'"); + if (handler != null) { + return handler.resolveConfig(req); + } + return null; + } + + public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException { + return handler.getConfig(configClass, applicationId, configId); + } + + @Override + public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> key, boolean recursive) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<String> allConfigIds(ApplicationId appID, Optional<Version> vespaVersion) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) { + return enabled && appId.equals(ApplicationId.global()); + } + + @Override + public ApplicationId resolveApplicationId(String hostName) { + return ApplicationId.global(); + } + + public void enable() { + enabled = true; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java new file mode 100644 index 00000000000..589363467b0 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelGenerationCounter.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.path.Path; +import com.yahoo.vespa.config.GenerationCounter; +import com.yahoo.vespa.curator.recipes.CuratorCounter; +import com.yahoo.vespa.curator.Curator; + +/** + * Distributed global generation counter for the super model. + * + * @author lulf + * @since 5.9 + */ +public class SuperModelGenerationCounter implements GenerationCounter { + + private static final Path counterPath = Path.fromString("/config/v2/RPC/superModelGeneration"); + private final CuratorCounter counter; + + public SuperModelGenerationCounter(Curator curator) { + this.counter = new CuratorCounter(curator, counterPath.getAbsolute()); + } + + /** + * Increment counter and return next value. This method is thread safe and provides an atomic value + * across zookeeper clusters. + * + * @return incremented counter value. + */ + public synchronized long increment() { + return counter.next(); + } + + /** + * @return current counter value. + */ + public synchronized long get() { + return counter.get(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java new file mode 100644 index 00000000000..c745379f9cb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/SuperModelRequestHandler.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.ConfigurationRuntimeException; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.server.model.SuperModel; + +import java.io.IOException; +import java.io.StringReader; + +/** + * Handler for global configs that must be resolved using the global SuperModel instance. Deals with + * reloading of config as well. + * + * @author lulf + * @since 5.9 + */ +public class SuperModelRequestHandler { + private final SuperModel model; + private final long generation; + private final ConfigDefinitionRepo configDefinitionRepo; + private final ConfigResponseFactory responseFactory; + + public SuperModelRequestHandler(SuperModel model, ConfigDefinitionRepo configDefinitionRepo, long generation, ConfigResponseFactory responseFactory) { + this.model = model; + this.configDefinitionRepo = configDefinitionRepo; + this.generation = generation; + this.responseFactory = responseFactory; + } + + /** + * Resolves global config for given request. + * + * @param request The {@link com.yahoo.vespa.config.GetConfigRequest} to find config for. + * @return a {@link com.yahoo.vespa.config.protocol.ConfigResponse} containing the response for this request. + * @throws java.lang.IllegalArgumentException if no such config was found. + */ + public ConfigResponse resolveConfig(GetConfigRequest request) { + ConfigKey<?> configKey = request.getConfigKey(); + InnerCNode targetDef = getConfigDefinition(request.getConfigKey(), request.getDefContent()); + try { + ConfigPayload payload = model.getConfig(configKey); + return responseFactory.createResponse(payload, targetDef, generation); + } catch (IOException e) { + throw new ConfigurationRuntimeException("Unable to resolve config", e); + } + } + + private InnerCNode getConfigDefinition(ConfigKey<?> configKey, DefContent defContent) { + if (defContent.isEmpty()) { + ConfigDefinitionKey configDefinitionKey = new ConfigDefinitionKey(configKey.getName(), configKey.getNamespace()); + ConfigDefinition configDefinition = configDefinitionRepo.getConfigDefinitions().get(configDefinitionKey); + if (configDefinition == null) { + throw new UnknownConfigDefinitionException("Unable to find config definition for '" + configKey.getNamespace() + "." + configKey.getName()); + } + return configDefinition.getCNode(); + } else { + DefParser dParser = new DefParser(configKey.getName(), new StringReader(defContent.asString())); + return dParser.getTree(); + } + } + + SuperModel getSuperModel() { + return model; + } + + long getGeneration() { return generation; } + + public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException { + return model.getConfig(configClass, applicationId, configId); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java new file mode 100644 index 00000000000..cabf4bed323 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenant.java @@ -0,0 +1,182 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Deployer; +import com.yahoo.config.provision.TenantName; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; +import com.yahoo.vespa.config.server.session.SessionFactory; +import com.yahoo.vespa.curator.Curator; + +import java.time.Duration; +import java.util.logging.Logger; + +/** + * Contains all tenant-level components for a single tenant, dealing with editing sessions and + * applications for a single tenant. + * + * @author vegardh + * @author lulf + * @since 5.1.26 + */ +public class Tenant implements TenantHandlerProvider { + + private static final Logger log = Logger.getLogger(Tenant.class.getName()); + static final String SESSIONS = "sessions"; + static final String APPLICATIONS = "applications"; + + private final TenantName name; + private final RemoteSessionRepo remoteSessionRepo; + private final Path path; + private final SessionFactory sessionFactory; + private final LocalSessionRepo localSessionRepo; + private final ApplicationRepo applicationRepo; + private final ActivateLock activateLock; + private final RequestHandler requestHandler; + private final ReloadHandler reloadHandler; + private final TenantFileSystemDirs tenantFileSystemDirs; + private final Curator curator; + + Tenant(TenantName name, + Path path, + SessionFactory sessionFactory, + LocalSessionRepo localSessionRepo, + RemoteSessionRepo remoteSessionRepo, + RequestHandler requestHandler, + ReloadHandler reloadHandler, + ApplicationRepo applicationRepo, + Curator curator, + TenantFileSystemDirs tenantFileSystemDirs) { + this.name = name; + this.path = path; + this.requestHandler = requestHandler; + this.reloadHandler = reloadHandler; + this.remoteSessionRepo = remoteSessionRepo; + this.sessionFactory = sessionFactory; + this.localSessionRepo = localSessionRepo; + this.activateLock = new ActivateLock(curator, path); + this.applicationRepo = applicationRepo; + this.tenantFileSystemDirs = tenantFileSystemDirs; + this.curator = curator; + } + + /** + * The reload handler for this + * + * @return handler + */ + public ReloadHandler getReloadHandler() { + return reloadHandler; + } + + /** + * The request handler for this + * + * @return handler + */ + public RequestHandler getRequestHandler() { + return requestHandler; + } + + /** + * The RemoteSessionRepo for this + * + * @return repo + */ + public RemoteSessionRepo getRemoteSessionRepo() { + return remoteSessionRepo; + } + + public TenantName getName() { + return name; + } + + public Path getPath() { + return path; + } + + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + public LocalSessionRepo getLocalSessionRepo() { + return localSessionRepo; + } + + /** + * The activation lock for this + * @return lock + */ + public ActivateLock getActivateLock() { + return activateLock; + } + + @Override + public String toString() { + return getName().value(); + } + + public ApplicationRepo getApplicationRepo() { + return applicationRepo; + } + + public Curator getCurator() { + return curator; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Tenant)) { + return false; + } + Tenant that = (Tenant) other; + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + /** + * Closes any watchers, thread pools that may react to changes in tenant state. Also removes any local state + * in filesystem. + */ + public void close() { + tenantFileSystemDirs.delete(); + remoteSessionRepo.close(); + applicationRepo.close(); + } + + /** + * Deletes a tenant from ZooKeeper and filesystem. + */ + public void delete() { + localSessionRepo.deleteAllSessions(); + curator.delete(path); + } + + public void redeployApplications(Deployer deployer) { + // TODO: Configurable timeout + applicationRepo.listApplications().stream() + .forEach(applicationId -> redeployApplication(applicationId, deployer)); + } + + private void redeployApplication(ApplicationId applicationId, Deployer deployer) { + try { + log.log(LogLevel.DEBUG, "Redeploying " + applicationId); + deployer.deployFromLocalActive(applicationId, Duration.ofMinutes(30)) + .ifPresent(deployment -> { + deployment.prepare(); + deployment.activate(); + }); + } catch (Exception e) { + log.log(LogLevel.ERROR, "Redeploying " + applicationId + " failed", e); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java new file mode 100644 index 00000000000..78ec368f2fb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantBuilder.java @@ -0,0 +1,201 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.path.Path; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.application.ZKApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.session.*; +import com.yahoo.vespa.config.server.zookeeper.SessionCounter; +import com.yahoo.vespa.defaults.Defaults; + +import java.io.File; +import java.time.Clock; +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Builder for helping out with tenant creation. Each of a tenants dependencies may be overridden for testing. + * + * @author lulf + * @since 5.1 + */ +public class TenantBuilder { + private final Path tenantPath; + private final GlobalComponentRegistry componentRegistry; + private final TenantName tenant; + private final Path sessionsPath; + private RemoteSessionRepo remoteSessionRepo; + private LocalSessionRepo localSessionRepo; + private SessionFactory sessionFactory; + private LocalSessionLoader localSessionLoader; + private ApplicationRepo applicationRepo; + private SessionCounter sessionCounter; + private ReloadHandler reloadHandler; + private RequestHandler requestHandler; + private RemoteSessionFactory remoteSessionFactory; + private TenantFileSystemDirs tenantFileSystemDirs; + private HostValidator<ApplicationId> hostValidator; + + private TenantBuilder(GlobalComponentRegistry componentRegistry, TenantName tenant, Path zkPath) { + this.componentRegistry = componentRegistry; + this.tenantPath = zkPath; + this.tenant = tenant; + this.sessionsPath = tenantPath.append(Tenant.SESSIONS); + } + + public static TenantBuilder create(GlobalComponentRegistry componentRegistry, TenantName tenant, Path zkPath) { + return new TenantBuilder(componentRegistry, tenant, zkPath); + } + + public TenantBuilder withSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + return this; + } + + public TenantBuilder withLocalSessionRepo(LocalSessionRepo localSessionRepo) { + this.localSessionRepo = localSessionRepo; + return this; + } + + public TenantBuilder withRemoteSessionRepo(RemoteSessionRepo remoteSessionRepo) { + this.remoteSessionRepo = remoteSessionRepo; + return this; + } + + public TenantBuilder withApplicationRepo(ApplicationRepo applicationRepo) { + this.applicationRepo = applicationRepo; + return this; + } + + public TenantBuilder withRequestHandler(RequestHandler requestHandler) { + this.requestHandler = requestHandler; + return this; + } + + public TenantBuilder withReloadHandler(ReloadHandler reloadHandler) { + this.reloadHandler = reloadHandler; + return this; + } + + /** + * Create a real tenant from the properties given by this builder. + * + * @return a new {@link Tenant} instance. + * @throws Exception if building fails + */ + public Tenant build() throws Exception { + createTenantRequestHandler(); + createApplicationRepo(); + createRemoteSessionFactory(); + createRemoteSessionRepo(); + createSessionCounter(); + createServerDbDirs(); + createSessionFactory(); + createLocalSessionRepo(); + return new Tenant(tenant, + tenantPath, + sessionFactory, + localSessionRepo, + remoteSessionRepo, + requestHandler, + reloadHandler, + applicationRepo, + componentRegistry.getCurator(), + tenantFileSystemDirs); + } + + private void createLocalSessionRepo() { + if (localSessionRepo == null) { + localSessionRepo = new LocalSessionRepo(tenantFileSystemDirs, localSessionLoader, applicationRepo, Clock.systemUTC(), componentRegistry.getConfigserverConfig().sessionLifetime()); + } + } + + private void createSessionFactory() { + if (sessionFactory == null || localSessionLoader == null) { + SessionFactoryImpl impl = new SessionFactoryImpl(componentRegistry, sessionCounter, sessionsPath, applicationRepo, tenantFileSystemDirs, hostValidator, tenant); + if (sessionFactory == null) { + sessionFactory = impl; + } + if (localSessionLoader == null) { + localSessionLoader = impl; + } + } + } + + private void createApplicationRepo() { + if (applicationRepo == null) { + applicationRepo = ZKApplicationRepo.create(componentRegistry.getCurator(), tenantPath.append(Tenant.APPLICATIONS), reloadHandler, tenant); + } + } + + private void createSessionCounter() { + if (sessionCounter == null) { + sessionCounter = new SessionCounter(componentRegistry.getCurator(), tenantPath, sessionsPath); + } + } + + private void createTenantRequestHandler() { + if (requestHandler == null || reloadHandler == null) { + TenantRequestHandler impl = new TenantRequestHandler(componentRegistry.getMetrics(), + tenant, + Collections.singletonList(componentRegistry.getReloadListener()), + ConfigResponseFactoryFactory.createFactory(componentRegistry.getConfigserverConfig()), + componentRegistry.getHostRegistries()); + if (hostValidator == null) { + this.hostValidator = impl; + } + if (requestHandler == null) { + requestHandler = impl; + } + if (reloadHandler == null) { + reloadHandler = impl; + } + } + } + + private void createRemoteSessionFactory() { + if (remoteSessionFactory == null) { + remoteSessionFactory = new RemoteSessionFactory( + componentRegistry, + sessionsPath, + tenant); + } + } + + private void createRemoteSessionRepo() throws Exception { + if (remoteSessionRepo == null) { + remoteSessionRepo = RemoteSessionRepo.create(componentRegistry.getCurator(), + remoteSessionFactory, + reloadHandler, + sessionsPath, + applicationRepo, + componentRegistry.getMetrics().getOrCreateMetricUpdater(Metrics.createDimensions(tenant)), + createSingleThreadedExecutorService(RemoteSessionRepo.class.getName())); + } + } + + private ExecutorService createSingleThreadedExecutorService(String executorName) { + return Executors.newSingleThreadExecutor(ThreadFactoryFactory.getThreadFactory(executorName + "-" + tenant.value())); + } + + private void createServerDbDirs() { + if (tenantFileSystemDirs == null) { + tenantFileSystemDirs = new TenantFileSystemDirs(new File(Defaults.getDefaults().underVespaHome(componentRegistry.getServerDB().getConfigserverConfig().configServerDBDir())), tenant); + } + } + + + public LocalSessionRepo getLocalSessionRepo() { + return localSessionRepo; + } + + public ApplicationRepo getApplicationRepo() { + return applicationRepo; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java new file mode 100644 index 00000000000..ab5bf6db7b9 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantDebugger.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.curator.Curator; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.cache.TreeCache; +import org.apache.curator.framework.recipes.cache.TreeCacheEvent; +import org.apache.curator.framework.recipes.cache.TreeCacheListener; + +import java.util.logging.Logger; + +/** + * For debugging tenant issues in configserver. Activate by loading component. + * + * @author lulf + */ +public class TenantDebugger implements TreeCacheListener { + private final TreeCache cache; + private static final Logger log = Logger.getLogger(TenantDebugger.class.getName()); + + public TenantDebugger(Curator curator) throws Exception { + cache = new TreeCache(curator.framework(), "/config/v2/tenants"); + cache.getListenable().addListener(this); + cache.start(); + } + + @Override + public void childEvent(CuratorFramework client, TreeCacheEvent event) throws Exception { + switch (event.getType()) { + case NODE_ADDED: + case NODE_REMOVED: + case NODE_UPDATED: + log.log(LogLevel.INFO, event.toString()); + break; + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java new file mode 100644 index 00000000000..a9321709dd3 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantHandlerProvider.java @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +/** + * Represents something that can provide request and reload handlers of a tenant. + * + * @author lulf + * @since 5.3 + */ +public interface TenantHandlerProvider { + + RequestHandler getRequestHandler(); + ReloadHandler getReloadHandler(); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java new file mode 100644 index 00000000000..7037e24ff5d --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantListener.java @@ -0,0 +1,32 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.provision.TenantName; + +/** + * Interface for something that listens for created and deleted tenants. + * + * @author lulf + * @since 5.8 + */ +public interface TenantListener { + /** + * Called whenever a new tenant is created. + * + * @param tenant name of newly created tenant. + * @param provider provider of request and reload handlers for new tenant. + */ + public void onTenantCreate(TenantName tenant, TenantHandlerProvider provider); + + /** + * Called whenever a tenant is deleted. + * + * @param tenant name of deleted tenant. + */ + public void onTenantDelete(TenantName tenant); + + /** + * Called when all tenants have been loaded at startup. + */ + void onTenantsLoaded(); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java new file mode 100644 index 00000000000..c4e77abc6e9 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TenantRequestHandler.java @@ -0,0 +1,213 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.util.*; + +import com.yahoo.config.provision.Version; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.*; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.http.NotFoundException; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; + +/** + * A per tenant request handler, for handling reload (activate application) and getConfig requests for + * a set of applications belonging to a tenant. + * + * @author Harald Musum + * @since 5.1 + */ +public class TenantRequestHandler implements RequestHandler, ReloadHandler, HostValidator<ApplicationId> { + + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(TenantRequestHandler.class.getName()); + + private final Metrics metrics; + private final TenantName tenant; + private final List<ReloadListener> reloadListeners; + private final ConfigResponseFactory responseFactory; + + private final HostRegistry<ApplicationId> hostRegistry; + private final ApplicationMapper applicationMapper = new ApplicationMapper(); + private final MetricUpdater tenantMetricUpdater; + + public TenantRequestHandler(Metrics metrics, + TenantName tenant, + List<ReloadListener> reloadListeners, + ConfigResponseFactory responseFactory, + HostRegistries hostRegistries) { + this.metrics = metrics; + this.tenant = tenant; + this.reloadListeners = reloadListeners; + this.responseFactory = responseFactory; + tenantMetricUpdater = metrics.getOrCreateMetricUpdater(Metrics.createDimensions(tenant)); + hostRegistry = hostRegistries.createApplicationHostRegistry(tenant); + } + + /** + * Gets a config for the given app, or null if not found + */ + @Override + public ConfigResponse resolveConfig(ApplicationId appId, GetConfigRequest req, Optional<Version> vespaVersion) { + Application application = getApplication(appId, vespaVersion); + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, Tenants.logPre(appId) + "Resolving for tenant '" + tenant + "' with handler for application '" + application + "'"); + } + return application.resolveConfig(req, responseFactory); + } + + // For testing only + long getApplicationGeneration(ApplicationId appId, Optional<Version> vespaVersion) { + Application application = getApplication(appId, vespaVersion); + return application.getApplicationGeneration(); + } + + private void notifyReloadListeners(ApplicationSet applicationSet) { + for (ReloadListener reloadListener : reloadListeners) { + reloadListener.hostsUpdated(tenant, hostRegistry.getAllHosts()); + reloadListener.configReloaded(tenant, applicationSet); + } + } + + /** + * Activates the config of the given app. Notifies listeners + * @param applicationSet the {@link com.yahoo.vespa.config.server.ApplicationSet} to be reloaded + */ + public void reloadConfig(ApplicationSet applicationSet) { + setLiveApp(applicationSet); + notifyReloadListeners(applicationSet); + } + + @Override + public void removeApplication(ApplicationId applicationId) { + if (applicationMapper.hasApplication(applicationId)) { + applicationMapper.remove(applicationId); + hostRegistry.removeHostsForKey(applicationId); + reloadListenersOnRemove(applicationId); + tenantMetricUpdater.setApplications(applicationMapper.numApplications()); + metrics.removeMetricUpdater(Metrics.createDimensions(applicationId)); + } + } + + private void reloadListenersOnRemove(ApplicationId applicationId) { + for (ReloadListener listener : reloadListeners) { + listener.applicationRemoved(applicationId); + listener.hostsUpdated(tenant, hostRegistry.getAllHosts()); + } + } + + private void setLiveApp(ApplicationSet applicationSet) { + ApplicationId id = applicationSet.getId(); + final Collection<String> hostsForApp = applicationSet.getAllHosts(); + hostRegistry.update(id, hostsForApp); + applicationSet.updateHostMetrics(); + tenantMetricUpdater.setApplications(applicationMapper.numApplications()); + applicationMapper.register(id, applicationSet); + } + + @Override + public Set<ConfigKey<?>> listNamedConfigs(ApplicationId appId, Optional<Version> vespaVersion, ConfigKey<?> keyToMatch, boolean recursive) { + Application application = getApplication(appId, vespaVersion); + return listConfigs(application, keyToMatch, recursive); + } + + private Set<ConfigKey<?>> listConfigs(Application application, ConfigKey<?> keyToMatch, boolean recursive) { + Set<ConfigKey<?>> ret = new LinkedHashSet<>(); + for (ConfigKey<?> key : application.allConfigsProduced()) { + String configId = key.getConfigId(); + if (recursive) { + key = new ConfigKey<>(key.getName(), configId, key.getNamespace()); + } else { + // Include first part of id as id + key = new ConfigKey<>(key.getName(), configId.split("/")[0], key.getNamespace()); + } + if (keyToMatch != null) { + String n = key.getName(); // Never null + String ns = key.getNamespace(); // Never null + if (n.equals(keyToMatch.getName()) && + ns.equals(keyToMatch.getNamespace()) && + configId.startsWith(keyToMatch.getConfigId()) && + !(configId.equals(keyToMatch.getConfigId()))) { + + if (!recursive) { + // For non-recursive, include the id segment we were searching for, and first part of the rest + key = new ConfigKey<>(key.getName(), appendOneLevelOfId(keyToMatch.getConfigId(), configId), key.getNamespace()); + } + ret.add(key); + } + } else { + ret.add(key); + } + } + return ret; + } + + @Override + public Set<ConfigKey<?>> listConfigs(ApplicationId appId, Optional<Version> vespaVersion, boolean recursive) { + Application application = getApplication(appId, vespaVersion); + return listConfigs(application, null, recursive); + } + + /** + * Given baseIdSegment search/ and id search/qrservers/default.0, return search/qrservers + * @return id segment with one extra level from the id appended + */ + String appendOneLevelOfId(String baseIdSegment, String id) { + if ("".equals(baseIdSegment)) return id.split("/")[0]; + String theRest = id.substring(baseIdSegment.length()); + if ("".equals(theRest)) return id; + theRest = theRest.replaceFirst("/", ""); + String theRestFirstSeg = theRest.split("/")[0]; + return baseIdSegment+"/"+theRestFirstSeg; + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced(ApplicationId appId, Optional<Version> vespaVersion) { + Application application = getApplication(appId, vespaVersion); + return application.allConfigsProduced(); + } + + private Application getApplication(ApplicationId appId, Optional<Version> vespaVersion) { + try { + return applicationMapper.getForVersion(appId, vespaVersion); + } catch (VersionDoesNotExistException ex) { + throw new NotFoundException(String.format("%sNo such application (id %s): %s", Tenants.logPre(tenant), appId, ex.getMessage())); + } + } + + @Override + public Set<String> allConfigIds(ApplicationId appId, Optional<Version> vespaVersion) { + Application application = getApplication(appId, vespaVersion); + return application.allConfigIds(); + } + + @Override + public boolean hasApplication(ApplicationId appId, Optional<Version> vespaVersion) { + return hasHandler(appId, vespaVersion); + } + + private boolean hasHandler(ApplicationId appId, Optional<Version> vespaVersion) { + return applicationMapper.hasApplicationForVersion(appId, vespaVersion); + } + + @Override + public ApplicationId resolveApplicationId(String hostName) { + ApplicationId applicationId = hostRegistry.getKeyForHost(hostName); + if (applicationId == null) { + applicationId = ApplicationId.defaultId(); + } + return applicationId; + } + + @Override + public void verifyHosts(ApplicationId key, Collection<String> newHosts) { + hostRegistry.verifyHosts(key, newHosts); + for (ReloadListener reloadListener : reloadListeners) { + reloadListener.verifyHostsAreAvailable(tenant, newHosts); + } + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java new file mode 100644 index 00000000000..63ae8d1f743 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/Tenants.java @@ -0,0 +1,358 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.google.inject.Inject; +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Deployer; +import com.yahoo.config.provision.TenantName; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.curator.Curator; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; +import org.apache.curator.framework.state.ConnectionState; +import org.apache.curator.framework.state.ConnectionStateListener; +import org.apache.zookeeper.KeeperException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * This component will monitor the set of tenants in the config server by watching in ZooKeeper. + * It will set up Tenant objects accordingly, which will manage the config sessions per tenant. + * This class will read the preexisting set of tenants from ZooKeeper at startup. (For now it will also + * create a default tenant since that will be used for API that do no know about tenants or have not yet + * implemented support for it). + * + * This instance is called from two different threads, the http handler threads and the zookeeper watcher threads. + * To create or delete a tenant, the handler calls {@link Tenants#createTenant} and {@link Tenants#deleteTenant} methods. + * This will delete shared state from zookeeper, and return, so it does not mean a tenant is immediately deleted. + * + * Once a tenant is deleted from zookeeper, the zookeeper watcher thread will get notified on all configservers, and + * shutdown and delete any per-configserver state. + * + * @author vegardh + * @author lulf + * @since 5.1.26 + */ +public class Tenants implements ConnectionStateListener, PathChildrenCacheListener { + + private static final Logger log = Logger.getLogger(Tenants.class.getName()); + + private static final TenantName DEFAULT_TENANT = TenantName.defaultName(); + private static final List<TenantName> SYSTEM_TENANT_NAMES = Arrays.asList( + DEFAULT_TENANT, + ApplicationId.HOSTED_VESPA_TENANT); + private static final Path tenantsPath = Path.fromString("/config/v2/tenants/"); + private static final Path vespaPath = Path.fromString("/vespa"); + + private final Map<TenantName, Tenant> tenants = new LinkedHashMap<>(); + private final GlobalComponentRegistry globalComponentRegistry; + private final List<TenantListener> tenantListeners = Collections.synchronizedList(new ArrayList<>()); + private final Curator curator; + + private final MetricUpdater metricUpdater; + private final ExecutorService pathChildrenExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(Tenants.class.getName())); + private final Curator.DirectoryCache directoryCache; + + + /** + * New instance from the tenants in the given component registry's ZooKeeper. Will set watch when reading them. + * + * @param globalComponentRegistry a {@link com.yahoo.vespa.config.server.GlobalComponentRegistry} + * @throws Exception is creating the Tenants instance fails + */ + @Inject + public Tenants(GlobalComponentRegistry globalComponentRegistry, Metrics metrics) throws Exception { + // Note: unit tests may want to use the constructor below to avoid setting watch by calling readTenants(). + this.globalComponentRegistry = globalComponentRegistry; + this.curator = globalComponentRegistry.getCurator(); + metricUpdater = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap()); + this.tenantListeners.add(globalComponentRegistry.getTenantListener()); + curator.framework().getConnectionStateListenable().addListener(this); + + curator.create(tenantsPath); + createSystemTenants(); + curator.create(vespaPath); + + this.directoryCache = globalComponentRegistry.getCurator().createDirectoryCache(tenantsPath.getAbsolute(), false, false, pathChildrenExecutor); + directoryCache.start(); + directoryCache.addListener(this); + tenantsChanged(readTenants()); + notifyTenantsLoaded(); + } + + private void notifyTenantsLoaded() { + for (TenantListener tenantListener : tenantListeners) { + tenantListener.onTenantsLoaded(); + } + } + + /** + * New instance containing the given tenants. This will not watch in ZooKeeper. + * @param globalComponentRegistry a {@link com.yahoo.vespa.config.server.GlobalComponentRegistry} instance + * @param metrics a {@link com.yahoo.vespa.config.server.monitoring.Metrics} instance + * @param tenants a collection of {@link Tenant}s + */ + public Tenants(GlobalComponentRegistry globalComponentRegistry, Metrics metrics, Collection<Tenant> tenants) { + this.globalComponentRegistry = globalComponentRegistry; + this.curator = globalComponentRegistry.getCurator(); + metricUpdater = metrics.getOrCreateMetricUpdater(Collections.<String, String>emptyMap()); + this.tenantListeners.add(globalComponentRegistry.getTenantListener()); + curator.create(tenantsPath); + this.directoryCache = curator.createDirectoryCache(tenantsPath.getAbsolute(), false, false, pathChildrenExecutor); + this.tenants.putAll(addTenants(tenants)); + } + + // Pre-condition: tenants path needs to exist in zk + private LinkedHashMap<TenantName, Tenant> addTenants(Collection<Tenant> newTenants) { + final LinkedHashMap<TenantName, Tenant> sessionTenants = new LinkedHashMap<>(); + for (Tenant t : newTenants) { + sessionTenants.put(t.getName(), t); + } + log.log(LogLevel.DEBUG, "Tenants at startup: " + sessionTenants); + metricUpdater.setTenants(tenants.size()); + return sessionTenants; + } + + /** + * Reads the set of tenants in patch cache. + * + * @return a set of tenant names + */ + private Set<TenantName> readTenants() { + Set<TenantName> tenants = new LinkedHashSet<>(); + for (String tenant : curator.getChildren(tenantsPath)) { + tenants.add(TenantName.from(tenant)); + } + return tenants; + } + + synchronized void tenantsChanged(Set<TenantName> newTenants) throws Exception { + log.log(LogLevel.DEBUG, "Tenants changed: " + newTenants); + checkForRemovedTenants(newTenants); + checkForAddedTenants(newTenants); + metricUpdater.setTenants(tenants.size()); + } + + private void checkForRemovedTenants(Set<TenantName> newTenants) { + Map<TenantName, Tenant> current = new LinkedHashMap<>(tenants); + for (Map.Entry<TenantName, Tenant> entry : current.entrySet()) { + TenantName tenant = entry.getKey(); + if (!newTenants.contains(tenant)) { + notifyRemovedTenant(tenant); + entry.getValue().close(); + tenants.remove(tenant); + } + } + } + + private void checkForAddedTenants(Set<TenantName> newTenants) + throws Exception { + ExecutorService executor = Executors.newFixedThreadPool(globalComponentRegistry.getConfigserverConfig().numParallelTenantLoaders()); + Map<TenantName, Tenant> addedTenants = new ConcurrentHashMap<>(); + for (TenantName tenantName : newTenants) { + // Note: the http handler will check if the tenant exists, and throw accordingly + if (!tenants.containsKey(tenantName)) { + executor.execute(() -> { + try { + Tenant tenant = TenantBuilder.create(globalComponentRegistry, tenantName, getTenantPath(tenantName)).build(); + notifyNewTenant(tenant); + addedTenants.put(tenantName, tenant); + } catch (Exception e) { + log.log(LogLevel.WARNING, "Error loading tenant '" + tenantName + "', skipping.", e); + } + }); + } + } + executor.shutdown(); + executor.awaitTermination(365, TimeUnit.DAYS); // Timeout should never happen + tenants.putAll(addedTenants); + } + + /** + * The registered tenants. Creates a copy of the map to avoid it being modified outside, since it can + * change after this method has been called. + * + * @return tenant list + */ + public synchronized Map<TenantName, Tenant> tenantsCopy() { + return new LinkedHashMap<>(tenants); + } + + /** + * Returns a default (compatibility with single tenant config requests) tenant + * + * @return default tenant + */ + public synchronized Tenant defaultTenant() { + return tenants.get(DEFAULT_TENANT); + } + + private void notifyNewTenant(Tenant tenant) { + for (TenantListener listener : tenantListeners) { + listener.onTenantCreate(tenant.getName(), tenant); + } + } + + private void notifyRemovedTenant(TenantName name) { + for (TenantListener listener : tenantListeners) { + listener.onTenantDelete(name); + } + } + + /** + * Writes the default tenant into ZooKeeper. Will not fail if the node already exists, + * as this is OK and might happen when several config servers start at the same time and + * try to call this method. + */ + public synchronized void createSystemTenants() { + for (final TenantName tenantName : SYSTEM_TENANT_NAMES) { + try { + createTenant(tenantName); + } catch (RuntimeException e) { + // Do nothing if we get NodeExistsException + if (e.getCause().getClass() != KeeperException.NodeExistsException.class) { + throw e; + } + } + } + } + + /** + * Writes the given tenant into ZooKeeper, for watchers to react on + * + * @param name name of the tenant + * @return this Tenants + */ + public synchronized Tenants createTenant(TenantName name) { + Path tenantPath = getTenantPath(name); + curator.createAtomically(tenantPath, tenantPath.append(Tenant.SESSIONS), tenantPath.append(Tenant.APPLICATIONS)); + return this; + } + + /** + * Removes the given tenant from ZooKeeper and filesystem. Assumes that tenant exists. + * + * @param name name of the tenant + * @return this Tenants instance + */ + public synchronized Tenants deleteTenant(TenantName name) { + Tenant tenant = tenants.get(name); + tenant.delete(); + return this; + } + + // For unit testing + String tenantZkPath(TenantName tenant) { + return getTenantPath(tenant).getAbsolute(); + } + + /** + * A helper to format a log preamble for messages with a tenant and app id + * @param app the app + * @return the log string + */ + public static String logPre(ApplicationId app) { + if (TenantName.defaultName().equals(app.tenant())) return ""; + StringBuilder ret = new StringBuilder() + .append(logPre(app.tenant())) + .append("app:"+app.application().value()) + .append(":"+app.instance().value()) + .append(" "); + return ret.toString(); + } + + /** + * A helper to format a log preamble for messages with a tenant + * @param tenant tenant + * @return the log string + */ + public static String logPre(TenantName tenant) { + if (DEFAULT_TENANT.equals(tenant)) return ""; + StringBuilder ret = new StringBuilder() + .append("tenant:"+tenant.value()) + .append(" "); + return ret.toString(); + } + + @Override + public void stateChanged(CuratorFramework framework, ConnectionState connectionState) { + switch (connectionState) { + case CONNECTED: + metricUpdater.incZKConnected(); + break; + case SUSPENDED: + metricUpdater.incZKSuspended(); + break; + case RECONNECTED: + metricUpdater.incZKReconnected(); + break; + case LOST: + metricUpdater.incZKConnectionLost(); + break; + case READ_ONLY: + // NOTE: Should not be relevant for configserver. + break; + } + } + + @Override + public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception { + switch (event.getType()) { + case CHILD_ADDED: + case CHILD_REMOVED: + tenantsChanged(readTenants()); + break; + } + } + + public void close() throws IOException { + directoryCache.close(); + pathChildrenExecutor.shutdown(); + } + + public void redeployApplications(Deployer deployer) { + final int totalNumberOfApplications = tenantsCopy().values().stream() + .mapToInt(tenant -> tenant.getApplicationRepo().listApplications().size()).sum(); + int applicationsRedeployed = 0; + for (Tenant tenant : tenantsCopy().values()) { + tenant.redeployApplications(deployer); + applicationsRedeployed += redeployProgress(tenant, applicationsRedeployed, totalNumberOfApplications); + } + } + + private static int redeployProgress(Tenant tenant, int applicationsRedeployed, int totalNumberOfApplications) { + int size = tenant.getApplicationRepo().listApplications().size(); + if (size > 0) { + log.log(LogLevel.INFO, String.format("Redeployed %s of %s applications", applicationsRedeployed + size, totalNumberOfApplications)); + } + return size; + } + + /** + * Gets zookeeper path for tenant data + * @param tenantName tenant name + * @return a {@link com.yahoo.path.Path} to the zookeeper data for a tenant + */ + public static Path getTenantPath(TenantName tenantName) { + return tenantsPath.append(tenantName.value()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java b/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java new file mode 100644 index 00000000000..fe02cc18bd8 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/TimeoutBudget.java @@ -0,0 +1,59 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.concurrent.TimeUnit; +import java.util.ArrayList; +import java.util.List; + +/** + * Handles a timeout logic by providing higher level abstraction for asking if there is time left. + * + * @author lulf + * @since 5.1 + */ +public class TimeoutBudget { + private final Clock clock; + private final Instant startTime; + private final List<Instant> measurements = new ArrayList<>(); + private final Instant endTime; + + public TimeoutBudget(Clock clock, Duration duration) { + this.clock = clock; + this.startTime = clock.instant(); + this.endTime = startTime.plus(duration); + } + + public Duration timeLeft() { + Instant now = clock.instant(); + measurements.add(now); + Duration duration = Duration.between(now, endTime); + return duration.isNegative() ? Duration.ofMillis(0) : duration; + } + + public boolean hasTimeLeft() { + Instant now = clock.instant(); + measurements.add(now); + return now.isBefore(endTime); + } + + public String timesUsed() { + StringBuilder buf = new StringBuilder(); + buf.append("["); + Instant prev = startTime; + for (Instant m : measurements) { + buf.append(Duration.between(prev, m).toMillis()); + prev = m; + buf.append(" ms, "); + } + Instant now = clock.instant(); + buf.append("total: "); + buf.append(Duration.between(startTime, now).toMillis()); + buf.append(" ms]"); + return buf.toString(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java new file mode 100644 index 00000000000..73e7acb5a88 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/UncompressedConfigResponseFactory.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +import com.yahoo.config.codegen.InnerCNode; +import com.yahoo.text.Utf8Array; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.protocol.CompressionInfo; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.SlimeConfigResponse; +import com.yahoo.vespa.config.util.ConfigUtils; + +/** + * Simply returns an uncompressed payload. + * + * @author lulf + * @since 5.19 + */ +public class UncompressedConfigResponseFactory implements ConfigResponseFactory { + @Override + public ConfigResponse createResponse(ConfigPayload payload, InnerCNode defFile, long generation) { + Utf8Array rawPayload = payload.toUtf8Array(true); + String configMd5 = ConfigUtils.getMd5(rawPayload); + CompressionInfo info = CompressionInfo.create(CompressionType.UNCOMPRESSED, rawPayload.getByteLength()); + return new SlimeConfigResponse(rawPayload, defFile, generation, configMd5, info); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java new file mode 100644 index 00000000000..182c25a84e2 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/UnknownConfigDefinitionException.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +/** + * Indicates that a config definition (typically a def file schema) was unknown to the config server + * + * @author lulf + * @since 5.1 + */ +public class UnknownConfigDefinitionException extends IllegalArgumentException { + public UnknownConfigDefinitionException(String s) { + super(s); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java new file mode 100644 index 00000000000..d6a329491e6 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/VersionDoesNotExistException.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server; + +/** + * @author Vegard Sjonfjell + */ +public final class VersionDoesNotExistException extends RuntimeException { + public VersionDoesNotExistException(String message) { + super(message); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java new file mode 100644 index 00000000000..7116dda7322 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/Application.java @@ -0,0 +1,243 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.ConfigurationRuntimeException; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Version; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.ConfigCacheKey; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.server.ConfigResponseFactory; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.UncompressedConfigResponseFactory; +import com.yahoo.vespa.config.server.UnknownConfigDefinitionException; +import com.yahoo.vespa.config.server.modelfactory.ModelResult; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.util.ConfigUtils; + +import java.io.IOException; +import java.util.Objects; +import java.util.Set; + +/** + * A Vespa application for a specific version of Vespa. It holds data and metadata associated with + * a Vespa application, i.e. generation, vespamodel instance and zookeeper data, as well as methods for resolving config + * and other queries against the model. + * + * @author Harald Musum + * @since 2010-12-08 + */ +public class Application implements ModelResult { + + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Application.class.getName()); + private final long appGeneration; // The generation of the set of configs belonging to an application + private final Version vespaVersion; + private final Model model; + private final ServerCache cache; + private final MetricUpdater metricUpdater; + private final ApplicationId app; + + public Application(Model model, ServerCache cache, long appGeneration, Version vespaVersion, MetricUpdater metricUpdater, ApplicationId app) { + Objects.requireNonNull(model, "The model cannot be null"); + this.model = model; + this.cache = cache; + this.appGeneration = appGeneration; + this.vespaVersion = vespaVersion; + this.metricUpdater = metricUpdater; + this.app = app; + } + + /** + * Returns the generation for the config we are currently serving + * + * @return the config generation + */ + public Long getApplicationGeneration() { return appGeneration; } + + // TODO: Return ApplicationName + public String getName() { return app.application().value(); } + + /** Returns the application model, never null */ + @Override + public Model getModel() { return model; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("application '").append(app.application().value()).append("', "); + sb.append("generation ").append(appGeneration).append(", "); + sb.append("vespa version ").append(vespaVersion); + return sb.toString(); + } + + public ServerCache getCache() { + return cache; + } + + public ApplicationId getId() { + return app; + } + + public Version getVespaVersion() { + return vespaVersion; + } + + /** + * The old style (deprecated) configs/ user overrides for this key + * + * @param key the key for the config to get user config for + * @return the user config value or null + */ + private ConfigPayload getLegacyUserConfigs(ConfigCacheKey key) { + try { + if (logDebug()) { + debug("Looking up legacy user config for " + key); + } + return cache.getLegacyUserConfig(key.getKey().getName()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Gets a config from ZK. Returns null if not found. + */ + public ConfigResponse resolveConfig(GetConfigRequest req, ConfigResponseFactory responseFactory) { + long start = System.currentTimeMillis(); + metricUpdater.incrementRequests(); + ConfigKey<?> configKey = req.getConfigKey(); + String defMd5 = configKey.getMd5(); + if (defMd5 == null || defMd5.isEmpty()) { + defMd5 = ConfigUtils.getDefMd5(req.getDefContent().asList()); + } + ConfigCacheKey cacheKey = new ConfigCacheKey(configKey, defMd5); + if (logDebug()) { + debug("Resolving config " + cacheKey); + } + + if (!req.noCache()) { + ConfigResponse config = cache.get(cacheKey); + if (config != null) { + if (logDebug()) { + debug("Found config " + cacheKey + " in cache"); + } + metricUpdater.incrementProcTime(System.currentTimeMillis() - start); + return config; + } + } + + // Try new ConfigInstance based API: + ConfigDefinitionWrapper configDefinitionWrapper = getTargetDef(req); + ConfigDefinition def = configDefinitionWrapper.getDef(); + if (def == null) { + metricUpdater.incrementFailedRequests(); + throw new UnknownConfigDefinitionException("Unable to find config definition for '" + configKey.getNamespace() + "." + configKey.getName()); + } + configKey = new ConfigKey<>(configDefinitionWrapper.getDefKey().getName(), configKey.getConfigId(), configDefinitionWrapper.getDefKey().getNamespace()); + ConfigPayload override = getLegacyUserConfigs(cacheKey); + ConfigPayload payload; + try { + if (logDebug()) { + debug("Resolving " + configKey + " with targetDef=" + def + ", override=" + override); + } + + payload = model.getConfig( + configKey, + def, + override); + } catch (IOException e) { + metricUpdater.incrementFailedRequests(); + throw new ConfigurationRuntimeException("Unable to resolve config", e); + } + + ConfigResponse configResponse = responseFactory.createResponse(payload, def.getCNode(), appGeneration); + metricUpdater.incrementProcTime(System.currentTimeMillis() - start); + if (!req.noCache()) { + cache.put(cacheKey, configResponse, configResponse.getConfigMd5()); + metricUpdater.setCacheConfigElems(cache.configElems()); + metricUpdater.setCacheChecksumElems(cache.checkSumElems()); + } + return configResponse; + } + + private boolean logDebug() { + return log.isLoggable(LogLevel.DEBUG); + } + + private void debug(String message) { + log.log(LogLevel.DEBUG, Tenants.logPre(getId())+message); + } + + private ConfigDefinitionWrapper getTargetDef(GetConfigRequest req) { + ConfigKey<?> configKey = req.getConfigKey(); + DefContent def = req.getDefContent(); + ConfigDefinitionKey configDefinitionKey = new ConfigDefinitionKey(configKey.getName(), configKey.getNamespace()); + if (def.isEmpty()) { + if (logDebug()) { + debug("No config schema in request for " + configKey); + } + ConfigDefinition ret = cache.getDef(configDefinitionKey); + return new ConfigDefinitionWrapper(configDefinitionKey, ret); + } else { + if (logDebug()) { + debug("Got config schema from request, length:" + def.asList().size() + " : " + configKey); + } + return new ConfigDefinitionWrapper(configDefinitionKey, new ConfigDefinition(configKey.getName(), def.asStringArray())); + } + } + + public void updateHostMetrics(int numHosts) { + metricUpdater.setHosts(numHosts); + } + + // For testing only + ConfigResponse resolveConfig(GetConfigRequest req) { + return resolveConfig(req, new UncompressedConfigResponseFactory()); + } + + public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, String configId) throws IOException { + ConfigKey<CONFIGTYPE> key = new ConfigKey<>(configClass, configId); + ConfigPayload payload = model.getConfig(key, (ConfigDefinition)null, null); + return payload.toInstance(configClass, configId); + } + + /** + * Wrapper class for holding config definition key and def, since when looking up + * we may end up changing the config definition key (fallback mechanism when using + * legacy config namespace (or not using config namespace)) + */ + private static class ConfigDefinitionWrapper { + private final ConfigDefinitionKey defKey; + private final ConfigDefinition def; + + ConfigDefinitionWrapper(ConfigDefinitionKey defKey, ConfigDefinition def) { + this.defKey = defKey; + this.def = def; + } + + public ConfigDefinitionKey getDefKey() { + return defKey; + } + + public ConfigDefinition getDef() { + return def; + } + } + + public Set<ConfigKey<?>> allConfigsProduced() { + return model.allConfigsProduced(); + } + + public Set<String> allConfigIds() { + return model.allConfigIds(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java new file mode 100644 index 00000000000..a36167517d8 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationConvergenceChecker.java @@ -0,0 +1,262 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.inject.Inject; +import com.yahoo.cloud.config.ModelConfig; +import com.yahoo.component.AbstractComponent; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import org.glassfish.jersey.client.proxy.WebResourceFactory; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * Checks for convergence of config generation for a given application. + * + * @author lulf + */ +public class ApplicationConvergenceChecker extends AbstractComponent { + private static final String statePath = "/state/v1/"; + private static final String configSubPath = "config"; + private static final String configPath = statePath + configSubPath; + private final StateApiFactory stateApiFactory; + private final Client client = ClientBuilder.newClient(); + + private final static Set<String> serviceTypes = new HashSet<>(Arrays.asList( + "container", + "qrserver", + "docprocservice", + "searchnode", + "storagenode", + "distributor" + )); + + @Inject + public ApplicationConvergenceChecker() { + this(ApplicationConvergenceChecker::createStateApi); + } + + public ApplicationConvergenceChecker(StateApiFactory stateApiFactory) { + this.stateApiFactory = stateApiFactory; + } + + // TODO: Remove this function once the other has taken over (list) + private void waitForConfigConverged(ModelConfig config, long wantedGeneration, TimeoutBudget timeoutBudget) { + + config.hosts().stream() + .forEach(host -> host.services().stream() + .filter(service -> serviceTypes.contains(service.type())) + .forEach(service -> { + Optional<Integer> statePort = getStatePort(service); + if (statePort.isPresent()) { + URI serviceUri = getServiceUri(host.name(), statePort.get()); + try { + waitForServiceGenerationConverged(serviceUri, wantedGeneration, timeoutBudget); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + })); + } + + public void waitForConfigConverged(Application application, TimeoutBudget timeoutBudget) throws IOException { + ModelConfig config = application.getConfig(ModelConfig.class, ""); + waitForConfigConverged(config, application.getApplicationGeneration(), timeoutBudget); + } + + private Optional<Integer> getStatePort(ModelConfig.Hosts.Services service) { return service.ports().stream() + .filter(port -> port.tags().contains("state")) + .map(ModelConfig.Hosts.Services.Ports::number) + .findFirst(); + } + + private URI getServiceUri(String host, int port) { + return URI.create("http://" + host + ":" + port); + } + + private void waitForServiceGenerationConverged(URI serviceUri, long wantedGeneration, TimeoutBudget timeoutBudget) throws InterruptedException { + long generation = -1; + do { + try { + generation = getServiceGeneration(serviceUri); + if (generation >= wantedGeneration) + return; + } catch (Exception e) { + // Try again + } + Thread.sleep(100); + } while (timeoutBudget.hasTimeLeft()); + StringBuilder message = new StringBuilder("Timed out waiting for service to use config generation "). + append(wantedGeneration). + append(" (checking "). + append(serviceUri).append(configPath). + append("), "); + if (generation == -1) { + message.append("could not connect."); + } else { + message.append("generation was ").append(generation).append("."); + } + throw new ConfigNotConvergedException(message.toString()); + } + + public long generationFromContainerState(JsonNode state) { + return state.get("config").get("generation").asLong(); + } + + private static StateApi createStateApi(Client client, URI uri) { + WebTarget target = client.target(uri); + return WebResourceFactory.newResource(StateApi.class, target); + } + + private long getServiceGeneration(URI serviceUri) { + StateApi state = stateApiFactory.createStateApi(client, serviceUri); + return generationFromContainerState(state.config()); + } + + @Override + public void deconstruct() { + client.close(); + } + + private boolean hostInApplication(Application application, String hostPort) { + final ModelConfig config; + try { + config = application.getConfig(ModelConfig.class, ""); + } catch (IOException e) { + throw new RuntimeException(e); + } + final List<ModelConfig.Hosts> hosts = config.hosts(); + for (ModelConfig.Hosts host : hosts) { + if (hostPort.startsWith(host.name())) { + for (ModelConfig.Hosts.Services service : host.services()) { + for (ModelConfig.Hosts.Services.Ports port : service.ports()) { + if (hostPort.equals(host.name() + ":" + port.number())) { + return true; + } + } + } + } + } + return false; + } + + public HttpResponse nodeConvergenceCheck(Application application, String hostFromRequest, URI uri) { + JSONObject answer = new JSONObject(); + JSONObject debug = new JSONObject(); + try { + answer.put("url", uri); + debug.put("wantedGeneration", application.getApplicationGeneration()); + debug.put("host", hostFromRequest); + + if (!hostInApplication(application, hostFromRequest)) { + debug.put("problem", "Host:port (service) no longer part of application, refetch list of services."); + answer.put("debug", debug); + return new JsonHttpResponse(410, answer); + } + final long generation = getServiceGeneration(URI.create("http://" + hostFromRequest)); + debug.put("currentGeneration", generation); + answer.put("debug", debug); + answer.put("converged", generation >= application.getApplicationGeneration()); + return new JsonHttpResponse(200, answer); + } catch(JSONException e) { + try { + answer.put("error", e.getMessage()); + } catch (JSONException e1) { + throw new RuntimeException("Fail while creating error message ", e1); + } + return new JsonHttpResponse(500, answer); + } + } + + private static class JsonHttpResponse extends HttpResponse { + + private final JSONObject answer; + + JsonHttpResponse(int returncode, JSONObject answer) { + super(returncode); + this.answer = answer; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + outputStream.write(answer.toString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + } + + public HttpResponse listConfigConvergence(Application application, URI uri) { + final JSONObject answer = new JSONObject(); + final JSONArray nodes = new JSONArray(); + final ModelConfig config; + try { + config = application.getConfig(ModelConfig.class, ""); + } catch (IOException e) { + throw new RuntimeException("failed on get config model", e); + } + config.hosts().stream() + .forEach(host -> { + host.services().stream() + .filter(service -> serviceTypes.contains(service.type())) + .forEach(service -> { + Optional<Integer> statePort = getStatePort(service); + if (statePort.isPresent()) { + JSONObject hostNode = new JSONObject(); + try { + hostNode.put("host", host.name()); + hostNode.put("port", statePort.get()); + hostNode.put("url", uri.toString() + "/" + host.name() + ":" + statePort.get()); + hostNode.put("type", service.type()); + + } catch (JSONException e) { + throw new RuntimeException(e); + } + nodes.put(hostNode); + } + }); + }); + try { + answer.put("services", nodes); + JSONObject debug = new JSONObject(); + debug.put("wantedVersion", application.getApplicationGeneration()); + answer.put("debug", debug); + answer.put("url", uri.toString()); + return new JsonHttpResponse(200, answer); + } catch (JSONException e) { + try { + answer.put("error", e.getMessage()); + } catch (JSONException e1) { + throw new RuntimeException("Failed while creating error message ", e1); + } + return new JsonHttpResponse(500, answer); + } + } + + @Path(statePath) + public interface StateApi { + @Path(configSubPath) + @GET + JsonNode config(); + } + + public interface StateApiFactory { + StateApi createStateApi(Client client, URI serviceUri); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java new file mode 100644 index 00000000000..a9296f26eeb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ApplicationRepo.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.transaction.Transaction; + +import java.util.List; + +/** + * Manages the list of active applications in a config server. + * + * @author lulf + * @since 5.1 + */ +public interface ApplicationRepo { + + /** + * List the active applications in this config server. + * + * @return a list of {@link com.yahoo.config.provision.ApplicationId}s that are active. + */ + public List<ApplicationId> listApplications(); + + /** + * Register active application and adds it to the repo. If it already exists it is overwritten. + * + * @param applicationId An {@link com.yahoo.config.provision.ApplicationId} that represents an active application. + * @param sessionId Id of the session containing the application package for this id. + */ + Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId); + + /** + * Return the stored session id for a given application. + * + * @param applicationId an {@link ApplicationId} + * @return session id of given application id. + * @throws IllegalArgumentException if the application does not exist + */ + long getSessionIdForApplication(ApplicationId applicationId); + + /** + * Deletes an application from the repo if it exists. + * + * @param applicationId an {@link ApplicationId} to delete. + */ + void deleteApplication(ApplicationId applicationId); + + /** + * Closes the application repo. Once a repo has been closed, it should not be used again. + */ + void close(); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java new file mode 100644 index 00000000000..91e57d504a5 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ConfigNotConvergedException.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +/** + * @author lulf + */ +public class ConfigNotConvergedException extends RuntimeException { + public ConfigNotConvergedException(String message) { + super(message); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java new file mode 100644 index 00000000000..bd60527ca79 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/LogServerLogGrabber.java @@ -0,0 +1,120 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.cloud.config.ModelConfig; +import com.yahoo.component.AbstractComponent; +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.HttpErrorResponse; +import com.yahoo.yolean.Exceptions; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +/** + * Fetches log entries from logserver with level errors and fatal. The logserver only return + * a log entry once over this API so doing repeated call will not give the same results. + * + * @author dybdahl + */ +public class LogServerLogGrabber extends AbstractComponent { + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogServerLogGrabber.class.getName()); + + @Inject + public LogServerLogGrabber() {} + + private Optional<Integer> getErrorLogPort(ModelConfig.Hosts.Services service) { + return service.ports().stream() + .filter(port -> port.tags().toLowerCase().contains("last-errors-holder")) + .map(ModelConfig.Hosts.Services.Ports::number) + .findFirst(); + } + + private class LogServerConnectionInfo { + String hostName; + int port; + } + + public HttpResponse grabLog(Application application) { + + final ModelConfig config; + try { + config = application.getConfig(ModelConfig.class, ""); + } catch (IOException e) { + throw new RuntimeException(e); + } + + final LogServerConnectionInfo logServerConnectionInfo = new LogServerConnectionInfo(); + + config.hosts().stream() + .forEach(host -> host.services().stream() + .filter(service -> service.type().equals("logserver")) + .forEach(logService -> { + Optional<Integer> logPort = getErrorLogPort(logService); + if (logPort.isPresent()) { + if (logServerConnectionInfo.hostName != null) { + throw new RuntimeException("Found several log server ports."); + } + logServerConnectionInfo.hostName = host.name(); + logServerConnectionInfo.port = logPort.get(); + } + })); + + if (logServerConnectionInfo.hostName == null) { + return new HttpResponse(503) { + @Override + public void render(OutputStream outputStream) throws IOException { + PrintWriter printWriter = new PrintWriter(outputStream); + printWriter.print("Did not find any log server in config model."); + printWriter.close(); + } + }; + } + log.log(LogLevel.DEBUG, "Requested error logs, pulling from logserver on " + logServerConnectionInfo.hostName + " " + + logServerConnectionInfo.port); + final String response; + try { + response = readLog(logServerConnectionInfo.hostName, logServerConnectionInfo.port); + log.log(LogLevel.DEBUG, "Requested error logs was " + response.length() + " characters"); + } catch (IOException e) { + return HttpErrorResponse.internalServerError(Exceptions.toMessageString(e)); + } + + return new HttpResponse(200) { + @Override + public void render(OutputStream outputStream) throws IOException { + outputStream.write(response.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + }; + } + + private String readLog(String host, int port) throws IOException { + Socket socket = new Socket(host, port); + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + StringBuilder data = new StringBuilder(); + + int bufferSize = 4096; + int charsRead; + do { + char[] buffer = new char[bufferSize]; + charsRead = in.read(buffer); + data.append(new String(buffer, 0, charsRead)); + } while (charsRead == bufferSize); + in.close(); + socket.close(); + return data.toString(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java new file mode 100644 index 00000000000..abffb002a76 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/PermanentApplicationPackage.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.log.LogLevel; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.vespa.defaults.Defaults; + +import java.io.File; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * A global permanent application package containing configuration info that is always used during deploy. + * + * @author lulf + * @since 5.15 + */ +public class PermanentApplicationPackage { + + private static final Logger log = Logger.getLogger(PermanentApplicationPackage.class.getName()); + private final Optional<ApplicationPackage> applicationPackage; + + public PermanentApplicationPackage(ConfigserverConfig config) { + File app = new File(Defaults.getDefaults().underVespaHome(config.applicationDirectory())); + applicationPackage = Optional.<ApplicationPackage>ofNullable(app.exists() ? + FilesApplicationPackage.fromFile(app) : null); + if (applicationPackage.isPresent()) { + log.log(LogLevel.DEBUG, "Detected permanent application package in '" + + Defaults.getDefaults().underVespaHome(config.applicationDirectory()) + + "'. This might add extra services to config models"); + } + } + + /** + * Get the permanent application package. + * + * @return An {@link Optional} of the application package, as it may not exist. + */ + public Optional<ApplicationPackage> applicationPackage() { + return applicationPackage; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java new file mode 100644 index 00000000000..6567f8439a5 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/application/ZKApplicationRepo.java @@ -0,0 +1,172 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.application; + +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.text.Utf8; +import com.yahoo.transaction.Transaction; +import com.yahoo.vespa.config.server.ReloadHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.transaction.CuratorOperations; +import com.yahoo.vespa.curator.transaction.CuratorTransaction; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent; +import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +/** + * Application repo backed by zookeeper. + * + * @author lulf + * @since 5.1 + */ +public class ZKApplicationRepo implements ApplicationRepo, PathChildrenCacheListener { + + private static final Logger log = Logger.getLogger(ZKApplicationRepo.class.getName()); + private final Curator curator; + private final Path root; + private final ExecutorService pathChildrenExecutor = Executors.newFixedThreadPool(1, ThreadFactoryFactory.getThreadFactory(ZKApplicationRepo.class.getName())); + private final Curator.DirectoryCache directoryCache; + private final ReloadHandler reloadHandler; + private final TenantName tenant; + + private ZKApplicationRepo(Curator curator, Path root, ReloadHandler reloadHandler, TenantName tenant) throws Exception { + this.curator = curator; + this.root = root; + this.reloadHandler = reloadHandler; + this.tenant = tenant; + rewriteApplicationIds(); + this.directoryCache = curator.createDirectoryCache(root.getAbsolute(), false, false, pathChildrenExecutor); + this.directoryCache.start(); + this.directoryCache.addListener(this); + } + + private void rewriteApplicationIds() { + try { + List<String> appNodes = curator.framework().getChildren().forPath(root.getAbsolute()); + for (String appNode : appNodes) { + Optional<ApplicationId> appId = parseApplication(appNode); + appId.filter(id -> shouldBeRewritten(appNode, id)) + .ifPresent(id -> rewriteApplicationId(id, appNode, readSessionId(id, appNode))); + } + } catch (Exception e) { + log.log(LogLevel.WARNING, "Error rewriting application ids on upgrade", e); + } + } + + private long readSessionId(ApplicationId appId, String appNode) { + String path = root.append(appNode).getAbsolute(); + try { + return Long.parseLong(Utf8.toString(curator.framework().getData().forPath(path))); + } catch (Exception e) { + throw new IllegalArgumentException(Tenants.logPre(appId) + "Unable to read the session id from '" + path + "'", e); + } + } + + private boolean shouldBeRewritten(String appNode, ApplicationId appId) { + return !appNode.equals(appId.serializedForm()); + } + + private void rewriteApplicationId(ApplicationId appId, String origNode, long sessionId) { + String newPath = root.append(appId.serializedForm()).getAbsolute(); + String oldPath = root.append(origNode).getAbsolute(); + try (CuratorTransaction transaction = new CuratorTransaction(curator)) { + if (curator.framework().checkExists().forPath(newPath) == null) { + transaction.add(CuratorOperations.create(newPath, Utf8.toAsciiBytes(sessionId))); + } + transaction.add(CuratorOperations.delete(oldPath)); + transaction.commit(); + } catch (Exception e) { + log.log(LogLevel.WARNING, "Error rewriting application id from " + origNode + " to " + appId.serializedForm()); + } + } + + public static ApplicationRepo create(Curator curator, Path root, ReloadHandler reloadHandler, TenantName tenant) { + try { + return new ZKApplicationRepo(curator, root, reloadHandler, tenant); + } catch (Exception e) { + throw new RuntimeException(Tenants.logPre(tenant)+"Error creating application repo", e); + } + } + + @Override + public List<ApplicationId> listApplications() { + try { + List<String> appNodes = curator.framework().getChildren().forPath(root.getAbsolute()); + List<ApplicationId> applicationIds = new ArrayList<>(); + for (String appNode : appNodes) { + parseApplication(appNode).ifPresent(applicationIds::add); + } + return applicationIds; + } catch (Exception e) { + throw new RuntimeException(Tenants.logPre(tenant)+"Unable to list applications", e); + } + } + + private Optional<ApplicationId> parseApplication(String appNode) { + try { + return Optional.of(ApplicationId.fromSerializedForm(tenant, appNode)); + } catch (IllegalArgumentException e) { + log.log(LogLevel.INFO, Tenants.logPre(tenant)+"Unable to parse application with id '" + appNode + "', ignoring."); + return Optional.empty(); + } + } + + @Override + public Transaction createPutApplicationTransaction(ApplicationId applicationId, long sessionId) { + if (listApplications().contains(applicationId)) { + return new CuratorTransaction(curator).add(CuratorOperations.setData(root.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId))); + } else { + return new CuratorTransaction(curator).add(CuratorOperations.create(root.append(applicationId.serializedForm()).getAbsolute(), Utf8.toAsciiBytes(sessionId))); + } + } + + @Override + public long getSessionIdForApplication(ApplicationId applicationId) { + return readSessionId(applicationId, applicationId.serializedForm()); + } + + @Override + public void deleteApplication(ApplicationId applicationId) { + Path path = root.append(applicationId.serializedForm()); + curator.delete(path); + } + + @Override + public void close() { + directoryCache.close(); + pathChildrenExecutor.shutdown(); + } + + + @Override + public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { + switch (event.getType()) { + case CHILD_ADDED: + applicationAdded(ApplicationId.fromSerializedForm(tenant, Path.fromString(event.getData().getPath()).getName())); + break; + case CHILD_REMOVED: + applicationRemoved(ApplicationId.fromSerializedForm(tenant, Path.fromString(event.getData().getPath()).getName())); + break; + } + } + + private void applicationRemoved(ApplicationId applicationId) { + reloadHandler.removeApplication(applicationId); + log.log(LogLevel.DEBUG, Tenants.logPre(applicationId)+"Application removed: " + applicationId); + } + + private void applicationAdded(ApplicationId applicationId) { + log.log(LogLevel.DEBUG, Tenants.logPre(applicationId)+"Application " + applicationId + " was added to repo"); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java new file mode 100644 index 00000000000..7c91a57c3af --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActions.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ConfigChangeAction; + +import java.util.List; + +/** + * Contains an aggregated view of which actions that must be performed to handle config + * changes between the current active model and the next model to prepare. + * The actions are split into restart and re-feed actions. + * + * @author geirst + * @since 5.44 + */ +public class ConfigChangeActions { + + private final RestartActions restartActions; + private final RefeedActions refeedActions; + + public ConfigChangeActions() { + this.restartActions = new RestartActions(); + this.refeedActions = new RefeedActions(); + } + + public ConfigChangeActions(List<ConfigChangeAction> actions) { + this.restartActions = new RestartActions(actions); + this.refeedActions = new RefeedActions(actions); + } + + public RestartActions getRestartActions() { + return restartActions; + } + + public RefeedActions getRefeedActions() { + return refeedActions; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java new file mode 100644 index 00000000000..5a426182b1a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/ConfigChangeActionsSlimeConverter.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.slime.Cursor; + +import java.util.Set; + +/** + * Class used to convert a ConfigChangeActions instance to Slime. + * + * @author geirst + * @since 5.44 + */ +public class ConfigChangeActionsSlimeConverter { + private final ConfigChangeActions actions; + + public ConfigChangeActionsSlimeConverter(ConfigChangeActions actions) { + this.actions = actions; + } + + public void toSlime(Cursor root) { + Cursor actionsCursor = root.setObject("configChangeActions"); + restartActionsToSlime(actionsCursor); + refeedActionsToSlime(actionsCursor); + } + + private void restartActionsToSlime(Cursor actionsCursor) { + Cursor restartCursor = actionsCursor.setArray("restart"); + for (RestartActions.Entry entry : actions.getRestartActions().getEntries()) { + Cursor entryCursor = restartCursor.addObject(); + entryCursor.setString("clusterName", entry.getClusterName()); + entryCursor.setString("clusterType", entry.getClusterType()); + entryCursor.setString("serviceType", entry.getServiceType()); + messagesToSlime(entryCursor, entry.getMessages()); + servicesToSlime(entryCursor, entry.getServices()); + } + } + + private void refeedActionsToSlime(Cursor actionsCursor) { + Cursor refeedCursor = actionsCursor.setArray("refeed"); + for (RefeedActions.Entry entry : actions.getRefeedActions().getEntries()) { + Cursor entryCursor = refeedCursor.addObject(); + entryCursor.setString("name", entry.name()); + entryCursor.setBool("allowed", entry.allowed()); + entryCursor.setString("documentType", entry.getDocumentType()); + entryCursor.setString("clusterName", entry.getClusterName()); + messagesToSlime(entryCursor, entry.getMessages()); + servicesToSlime(entryCursor, entry.getServices()); + } + } + + private static void messagesToSlime(Cursor entryCursor, Set<String> messages) { + Cursor messagesCursor = entryCursor.setArray("messages"); + for (String message : messages) { + messagesCursor.addString(message); + } + } + + private static void servicesToSlime(Cursor entryCursor, Set<ServiceInfo> services) { + Cursor servicesCursor = entryCursor.setArray("services"); + for (ServiceInfo service : services) { + Cursor serviceCursor = servicesCursor.addObject(); + serviceCursor.setString("serviceName", service.getServiceName()); + serviceCursor.setString("serviceType", service.getServiceType()); + serviceCursor.setString("configId", service.getConfigId()); + serviceCursor.setString("hostName", service.getHostName()); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java new file mode 100644 index 00000000000..87b7b466fbe --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActions.java @@ -0,0 +1,91 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.model.api.ConfigChangeRefeedAction; +import com.yahoo.config.model.api.ServiceInfo; + +import java.util.*; + +/** + * Represents all actions to re-feed document types in order to handle config changes. + * + * @author geirst + * @since 5.44 + */ +public class RefeedActions { + + public static class Entry { + + private final String name; + private final boolean allowed; + private final String documentType; + private final String clusterName; + private final Set<ServiceInfo> services = new LinkedHashSet<>(); + private final Set<String> messages = new TreeSet<>(); + + private Entry(String name, boolean allowed, String documentType, String clusterName) { + this.name = name; + this.allowed = allowed; + this.documentType = documentType; + this.clusterName = clusterName; + } + + private Entry addService(ServiceInfo service) { + services.add(service); + return this; + } + + private Entry addMessage(String message) { + messages.add(message); + return this; + } + + public String name() { return name; } + + public boolean allowed() { return allowed; } + + public String getDocumentType() { return documentType; } + + public String getClusterName() { return clusterName; } + + public Set<ServiceInfo> getServices() { return services; } + + public Set<String> getMessages() { return messages; } + + } + + private Entry addEntry(String name, boolean allowed, String documentType, ServiceInfo service) { + String clusterName = service.getProperty("clustername").orElse(""); + String entryId = name + "." + allowed + "." + clusterName + "." + documentType; + Entry entry = actions.get(entryId); + if (entry == null) { + entry = new Entry(name, allowed, documentType, clusterName); + actions.put(entryId, entry); + } + return entry; + } + + private final Map<String, Entry> actions = new TreeMap<>(); + + public RefeedActions() { + } + + public RefeedActions(List<ConfigChangeAction> actions) { + for (ConfigChangeAction action : actions) { + if (action.getType().equals(ConfigChangeAction.Type.REFEED)) { + ConfigChangeRefeedAction refeedAction = (ConfigChangeRefeedAction) action; + for (ServiceInfo service : refeedAction.getServices()) { + addEntry(refeedAction.name(), refeedAction.allowed(), refeedAction.getDocumentType(), service). + addService(service). + addMessage(action.getMessage()); + } + } + } + } + + public List<Entry> getEntries() { + return new ArrayList<>(actions.values()); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java new file mode 100644 index 00000000000..ae72c61bcdb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RefeedActionsFormatter.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +/** + * Class used to format re-feed actions for human readability. + * + * @author geirst + * @since 5.44 + */ +public class RefeedActionsFormatter { + + private final RefeedActions actions; + + public RefeedActionsFormatter(RefeedActions actions) { + this.actions = actions; + } + + public String format() { + StringBuilder builder = new StringBuilder(); + for (RefeedActions.Entry entry : actions.getEntries()) { + if (entry.allowed()) + builder.append("(allowed) "); + builder.append(entry.name() + ": Consider removing data and re-feed document type '" + entry.getDocumentType() + + "' in cluster '" + entry.getClusterName() + "' because:\n"); + int counter = 1; + for (String message : entry.getMessages()) { + builder.append(" " + (counter++) + ") " + message + "\n"); + } + } + return builder.toString(); + } + +}
\ No newline at end of file diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java new file mode 100644 index 00000000000..6c2c080e6e4 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActions.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.model.api.ServiceInfo; + +import java.util.*; + +/** + * Represents all actions to restart services in order to handle a config change. + * + * @author geirst + * @since 5.44 + */ +public class RestartActions { + + public static class Entry { + + private final String clusterName; + private final String clusterType; + private final String serviceType; + private final Set<ServiceInfo> services = new LinkedHashSet<>(); + private final Set<String> messages = new TreeSet<>(); + + private Entry addService(ServiceInfo service) { + services.add(service); + return this; + } + + private Entry addMessage(String message) { + messages.add(message); + return this; + } + + private Entry(String clusterName, String clusterType, String serviceType) { + this.clusterName = clusterName; + this.clusterType = clusterType; + this.serviceType = serviceType; + } + + public String getClusterName() { + return clusterName; + } + + public String getClusterType() { + return clusterType; + } + + public String getServiceType() { + return serviceType; + } + + public Set<ServiceInfo> getServices() { + return services; + } + + public Set<String> getMessages() { + return messages; + } + + } + + private Entry addEntry(ServiceInfo service) { + String clusterName = service.getProperty("clustername").orElse(""); + String clusterType = service.getProperty("clustertype").orElse(""); + String entryId = clusterType + "." + clusterName + "." + service.getServiceType(); + Entry entry = actions.get(entryId); + if (entry == null) { + entry = new Entry(clusterName, clusterType, service.getServiceType()); + actions.put(entryId, entry); + } + return entry; + } + + private final Map<String, Entry> actions = new TreeMap<>(); + + public RestartActions() { + } + + public RestartActions(List<ConfigChangeAction> actions) { + for (ConfigChangeAction action : actions) { + if (action.getType().equals(ConfigChangeAction.Type.RESTART)) { + for (ServiceInfo service : action.getServices()) { + addEntry(service). + addService(service). + addMessage(action.getMessage()); + } + } + } + } + + public List<Entry> getEntries() { + return new ArrayList<>(actions.values()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java new file mode 100644 index 00000000000..7d61f31ac47 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/configchange/RestartActionsFormatter.java @@ -0,0 +1,30 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.configchange; + +/** + * Class used to format restart actions for human readability. + * + * @author geirst + * @since 5.44 + */ +public class RestartActionsFormatter { + + private final RestartActions actions; + + public RestartActionsFormatter(RestartActions actions) { + this.actions = actions; + } + + public String format() { + StringBuilder builder = new StringBuilder(); + for (RestartActions.Entry entry : actions.getEntries()) { + builder.append("In cluster '" + entry.getClusterName() + "' of type '" + entry.getClusterType() + "':\n"); + builder.append(" Restart services of type '" + entry.getServiceType() + "' because:\n"); + int counter = 1; + for (String message : entry.getMessages()) { + builder.append(" " + counter++ + ") " + message + "\n"); + } + } + return builder.toString(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java new file mode 100644 index 00000000000..219e1008f3a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/counter/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author andreer + */ +@ExportPackage +package com.yahoo.vespa.config.server.counter; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java new file mode 100644 index 00000000000..00a036df9d5 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployer.java @@ -0,0 +1,80 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.vespa.config.server.ActivateLock; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.SilentDeployLogger; +import com.yahoo.vespa.curator.Curator; + +import java.time.Clock; +import java.time.Duration; +import java.util.Optional; + +/** + * The API for deploying applications. + * A class which needs to deploy applications can have an instance of this injected. + * + * @author bratseth + */ +public class Deployer implements com.yahoo.config.provision.Deployer { + + private final Tenants tenants; + private final Optional<Provisioner> hostProvisioner; + private final ConfigserverConfig configserverConfig; + private final Curator curator; + private final Clock clock; + private final DeployLogger logger = new SilentDeployLogger(); + + public Deployer(Tenants tenants, HostProvisionerProvider hostProvisionerProvider, + ConfigserverConfig configserverConfig, Curator curator) { + this.tenants = tenants; + this.hostProvisioner = hostProvisionerProvider.getHostProvisioner(); + this.configserverConfig = configserverConfig; + this.curator = curator; + this.clock = Clock.systemUTC(); + } + + /** + * Creates a new deployment from the active application, if available. + * + * @param application the active application to be redeployed + * @param timeout the timeout to use for each individual deployment operation + * @return a new deployment from the local active, or empty if a local active application + * was not present for this id (meaning it either is not active or active on another + * node in the config server cluster) + */ + @Override + public Optional<com.yahoo.config.provision.Deployment> deployFromLocalActive(ApplicationId application, Duration timeout) { + Tenant tenant = tenants.tenantsCopy().get(application.tenant()); + LocalSession activeSession = tenant.getLocalSessionRepo().getActiveSession(application); + if (activeSession == null) return Optional.empty(); + TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout); + LocalSession newSession = tenant.getSessionFactory().createSessionFromExisting(activeSession, logger, timeoutBudget); + tenant.getLocalSessionRepo().addSession(newSession); + return Optional.of(Deployment.unprepared(newSession, + tenant.getLocalSessionRepo(), + tenant.getPath(), + configserverConfig, + hostProvisioner, + new ActivateLock(curator, tenant.getPath()), + timeout, clock)); + } + + public Deployment deployFromPreparedSession(LocalSession session, ActivateLock lock, LocalSessionRepo localSessionRepo, Duration timeout) { + return Deployment.prepared(session, + localSessionRepo, + hostProvisioner, + lock, + timeout, clock); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java new file mode 100644 index 00000000000..cf366fd21f7 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/Deployment.java @@ -0,0 +1,202 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.HostFilter; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.transaction.NestedTransaction; +import com.yahoo.transaction.Transaction; +import com.yahoo.vespa.config.server.ActivateLock; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.http.InternalServerException; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.PrepareParams; +import com.yahoo.vespa.config.server.session.Session; +import com.yahoo.vespa.config.server.session.SilentDeployLogger; + +import java.time.Clock; +import java.time.Duration; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * The process of deploying an application. + * Deployments are created by a {@link Deployer}. + * Instances of this are not multithread safe. + * + * @author lulf + * @author bratseth + */ +public class Deployment implements com.yahoo.config.provision.Deployment { + + private static final Logger log = Logger.getLogger(Deployment.class.getName()); + + /** The session containing the application instance to activate */ + private final LocalSession session; + private final LocalSessionRepo localSessionRepo; + /** The path to the tenant, or null if not available (only used during prepare) */ + private final Path tenantPath; + /** The config server config, or null if not available (only used during prepare) */ + private final ConfigserverConfig configserverConfig; + private final Optional<Provisioner> hostProvisioner; + private final ActivateLock activateLock; + private final Duration timeout; + private final Clock clock; + private final DeployLogger logger = new SilentDeployLogger(); + + private boolean prepared = false; + + private boolean ignoreLockFailure = false; + private boolean ignoreSessionStaleFailure = false; + + private Deployment(LocalSession session, LocalSessionRepo localSessionRepo, Path tenantPath, ConfigserverConfig configserverConfig, + Optional<Provisioner> hostProvisioner, ActivateLock activateLock, + Duration timeout, Clock clock, boolean prepared) { + this.session = session; + this.localSessionRepo = localSessionRepo; + this.tenantPath = tenantPath; + this.configserverConfig = configserverConfig; + this.hostProvisioner = hostProvisioner; + this.activateLock = activateLock; + this.timeout = timeout; + this.clock = clock; + this.prepared = prepared; + } + + static Deployment unprepared(LocalSession session, LocalSessionRepo localSessionRepo, Path tenantPath, ConfigserverConfig configserverConfig, + Optional<Provisioner> hostProvisioner, ActivateLock activateLock, + Duration timeout, Clock clock) { + return new Deployment(session, localSessionRepo, tenantPath, configserverConfig, hostProvisioner, activateLock, + timeout, clock, false); + } + + static Deployment prepared(LocalSession session, LocalSessionRepo localSessionRepo, + Optional<Provisioner> hostProvisioner, ActivateLock activateLock, + Duration timeout, Clock clock) { + return new Deployment(session, localSessionRepo, null, null, hostProvisioner, activateLock, + timeout, clock, true); + } + + public Deployment setIgnoreLockFailure(boolean ignoreLockFailure) { + this.ignoreLockFailure = ignoreLockFailure; + return this; + } + + public Deployment setIgnoreSessionStaleFailure(boolean ignoreSessionStaleFailure) { + this.ignoreSessionStaleFailure = ignoreSessionStaleFailure; + return this; + } + + /** Prepares this. This does nothing if this is already prepared */ + @Override + public void prepare() { + if (prepared) return; + TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout); + session.prepare(logger, + /** Assumes that session has already set application id, see {@link com.yahoo.vespa.config.server.session.SessionFactoryImpl}. */ + new PrepareParams(configserverConfig).applicationId(session.getApplicationId()).timeoutBudget(timeoutBudget), + Optional.empty(), + tenantPath); + this.prepared = true; + } + + /** Activates this. If it is not already prepared, this will call prepare first. */ + @Override + public void activate() { + if (! prepared) + prepare(); + + TimeoutBudget timeoutBudget = new TimeoutBudget(clock, timeout); + long sessionId = session.getSessionId(); + validateSessionStatus(session); + try { + activateLock.acquire(timeoutBudget, ignoreLockFailure); + NestedTransaction transaction = new NestedTransaction(); + transaction.add(deactivateCurrentActivateNew(localSessionRepo.getActiveSession(session.getApplicationId()), session, ignoreSessionStaleFailure)); + if (hostProvisioner.isPresent() && !session.getApplicationId().isHostedVespaRoutingApplication()) { + ProvisionInfo info = session.getProvisionInfo(); + hostProvisioner.get().activate(transaction, session.getApplicationId(), info.getHosts()); + } + transaction.commit(); + session.waitUntilActivated(timeoutBudget); + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new InternalServerException("Error activating application", e); + } finally { + activateLock.release(); + } + final ApplicationMetaData metaData = session.getMetaData(); + log.log(LogLevel.INFO, session.logPre() + "Session " + sessionId + " activated successfully. Config generation " + metaData.getGeneration()); + } + + /** + * Request a restart of services of this application on hosts matching the filter. + * This is sometimes needed after activation, but can also be requested without + * doing prepare and activate in the same session. + */ + public void restart(HostFilter filter) { + hostProvisioner.get().restart(session.getApplicationId(), filter); + } + + private long validateSessionStatus(LocalSession localSession) { + long sessionId = localSession.getSessionId(); + if (Session.Status.NEW.equals(localSession.getStatus())) { + throw new IllegalStateException(localSession.logPre() + "Session " + sessionId + " is not prepared"); + } else if (Session.Status.ACTIVATE.equals(localSession.getStatus())) { + throw new IllegalArgumentException(localSession.logPre() + "Session " + sessionId + " is already active"); + } + return sessionId; + } + + private Transaction deactivateCurrentActivateNew(LocalSession currentActiveSession, LocalSession session, boolean ignoreStaleSessionFailure) { + Transaction transaction = session.createActivateTransaction(); + if (isValidSession(currentActiveSession)) { + checkIfActiveHasChanged(session, currentActiveSession, ignoreStaleSessionFailure); + checkIfActiveIsNewerThanSessionToBeActivated(session.getSessionId(), currentActiveSession.getSessionId()); + transaction.add(currentActiveSession.createDeactivateTransaction().operations()); + } + return transaction; + } + + private boolean isValidSession(LocalSession session) { + return session != null; + } + + private void checkIfActiveHasChanged(LocalSession session, LocalSession currentActiveSession, boolean ignoreStaleSessionFailure) { + long activeSessionAtCreate = session.getActiveSessionAtCreate(); + log.log(LogLevel.DEBUG, currentActiveSession.logPre()+"active session id at create time=" + activeSessionAtCreate); + if (activeSessionAtCreate == 0) return; // No active session at create + + long sessionId = session.getSessionId(); + long currentActiveSessionSessionId = currentActiveSession.getSessionId(); + log.log(LogLevel.DEBUG, currentActiveSession.logPre()+"sessionId=" + sessionId + ", current active session=" + currentActiveSessionSessionId); + if (currentActiveSession.isNewerThan(activeSessionAtCreate) && + currentActiveSessionSessionId != sessionId) { + String errMsg = currentActiveSession.logPre()+"Cannot activate session " + + sessionId + " because the currently active session (" + + currentActiveSessionSessionId + ") has changed since session " + sessionId + + " was created (was " + activeSessionAtCreate + " at creation time)"; + if (ignoreStaleSessionFailure) { + log.warning(errMsg+ " (Continuing because of force.)"); + } else { + throw new IllegalStateException(errMsg); + } + } + } + + // As of now, config generation is based on session id, and config generation must be an monotonically + // increasing number + private void checkIfActiveIsNewerThanSessionToBeActivated(long sessionId, long currentActiveSessionId) { + if (sessionId < currentActiveSessionId) { + throw new IllegalArgumentException("It is not possible to activate session " + sessionId + + ", because it is older than current active session (" + currentActiveSessionId + ")"); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java new file mode 100644 index 00000000000..9a6f373a807 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ModelContextImpl.java @@ -0,0 +1,160 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.config.model.api.*; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.Version; +import com.yahoo.config.provision.Zone; + +import java.io.File; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Implementation of {@link ModelContext} for configserver. + * + * @author lulf + */ +public class ModelContextImpl implements ModelContext { + private final ApplicationPackage applicationPackage; + private final Optional<Model> previousModel; + private final Optional<ApplicationPackage> permanentApplicationPackage; + private final DeployLogger deployLogger; + private final ConfigDefinitionRepo configDefinitionRepo; + private final FileRegistry fileRegistry; + private final Optional<HostProvisioner> hostProvisioner; + private final ModelContext.Properties properties; + private final Optional<File> appDir; + Optional<Version> vespaVersion; + + public ModelContextImpl(ApplicationPackage applicationPackage, + Optional<Model> previousModel, + Optional<ApplicationPackage> permanentApplicationPackage, + DeployLogger deployLogger, + ConfigDefinitionRepo configDefinitionRepo, + FileRegistry fileRegistry, + Optional<HostProvisioner> hostProvisioner, + ModelContext.Properties properties, + Optional<File> appDir, + Optional<Version> vespaVersion) { + this.applicationPackage = applicationPackage; + this.previousModel = previousModel; + this.permanentApplicationPackage = permanentApplicationPackage; + this.deployLogger = deployLogger; + this.configDefinitionRepo = configDefinitionRepo; + this.fileRegistry = fileRegistry; + this.hostProvisioner = hostProvisioner; + this.properties = properties; + this.appDir = appDir; + this.vespaVersion = vespaVersion; + } + + @Override + public ApplicationPackage applicationPackage() { + return applicationPackage; + } + + @Override + public Optional<Model> previousModel() { + return previousModel; + } + + @Override + public Optional<ApplicationPackage> permanentApplicationPackage() { + return permanentApplicationPackage; + } + + @Override + public Optional<HostProvisioner> hostProvisioner() { + return hostProvisioner; + } + + @Override + public DeployLogger deployLogger() { + return deployLogger; + } + + @Override + public ConfigDefinitionRepo configDefinitionRepo() { + return configDefinitionRepo; + } + + @Override + public FileRegistry getFileRegistry() { + return fileRegistry; + } + + @Override + public ModelContext.Properties properties() { + return properties; + } + + @Override + public Optional<File> appDir() { + return appDir; + } + + @Override + public Optional<Version> vespaVersion() { return vespaVersion; } + + /** + * @author lulf + */ + public static class Properties implements ModelContext.Properties { + private final ApplicationId applicationId; + private final boolean multitenant; + private final List<ConfigServerSpec> configServerSpecs; + private final boolean hostedVespa; + private final Zone zone; + private final Set<Rotation> rotations; + + public Properties(ApplicationId applicationId, + boolean multitenant, + List<ConfigServerSpec> configServerSpecs, + boolean hostedVespa, + Zone zone, + Set<Rotation> rotations) { + this.applicationId = applicationId; + this.multitenant = multitenant; + this.configServerSpecs = configServerSpecs; + this.hostedVespa = hostedVespa; + this.zone = zone; + this.rotations = rotations; + } + + @Override + public boolean multitenant() { + return multitenant; + } + + @Override + public ApplicationId applicationId() { + return applicationId; + } + + @Override + public List<ConfigServerSpec> configServerSpecs() { + return configServerSpecs; + } + + @Override + public boolean hostedVespa() { + return hostedVespa; + } + + @Override + public Zone zone() { + return zone; + } + + @Override + public Set<Rotation> rotations() { + return rotations; + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java new file mode 100644 index 00000000000..336b50351bb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/TenantFileSystemDirs.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.google.common.io.Files; +import com.yahoo.config.provision.TenantName; +import com.yahoo.io.IOUtils; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.ConfigServerDB; + +import java.io.File; + +/* + * Holds file system directories for a tenant + * + * @author tonytv + */ +public class TenantFileSystemDirs { + + private final File serverDB; + private final TenantName tenant; + + public TenantFileSystemDirs(File dir, TenantName tenant) { + this.serverDB = dir; + this.tenant = tenant; + ConfigServerDB.cr(path()); + } + + public static TenantFileSystemDirs createTestDirs(TenantName tenantName) { + return new TenantFileSystemDirs(Files.createTempDir(), tenantName); + } + + public File path() { + return new File(serverDB, Path.fromString("tenants").append(tenant.value()).append("sessions").getRelative()); + } + + public File getUserApplicationDir(long generation) { + return new File(path(), String.valueOf(generation)); + } + + public String getPath() { + return serverDB.getPath(); + } + + public void delete() { + IOUtils.recursiveDeleteDir(new File(serverDB, Path.fromString("tenants").append(tenant.value()).getRelative())); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java new file mode 100644 index 00000000000..9c6f21f3511 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperClient.java @@ -0,0 +1,378 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.UnparsedConfigDefinition; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.Version; +import com.yahoo.io.reader.NamedReader; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.config.model.application.provider.PreGeneratedFileRegistry; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import org.apache.commons.io.IOUtils; + +import java.io.*; +import java.util.*; + +/** + * A class used for reading and writing application data to zookeeper. + * + * @author <a href="mailto:musum@yahoo-inc.com">Harald Musum</a> + * @since 5.1 + */ +public class ZooKeeperClient { + + private final ConfigCurator configCurator; + private final DeployLogger logger; + private final boolean trace; + /* This is the generation that will be used for reading and writing application data. (1 more than last deployed application) */ + private final Path rootPath; + + static final ApplicationFile.PathFilter xmlFilter = new ApplicationFile.PathFilter() { + @Override + public boolean accept(Path path) { + return path.getName().endsWith(".xml"); + } + }; + + public ZooKeeperClient(ConfigCurator configCurator, DeployLogger logger, boolean trace, Path rootPath) { + this.configCurator = configCurator; + this.logger = logger; + this.trace = trace; + this.rootPath = rootPath; + } + + /** + * Sets up basic node structure in ZooKeeper and purges old data. + * This is the first operation on ZK during deploy-application. + * + * We have retries in this method because there have been cases of stray connection loss to ZK, + * even though the user has started the config server. + * + */ + void setupZooKeeper() { + int retries = 5; + try { + while (retries > 0) { + try { + trace("Setting up ZooKeeper nodes for this application"); + createZooKeeperNodes(); + break; + } catch (RuntimeException e) { + logger.log(LogLevel.FINE, "ZK init failed, retrying: " + e); + retries--; + if (retries == 0) { + throw e; + } + Thread.sleep(100); + // Not reconnecting, ZK is supposed to handle that automatically + // as long as the session doesn't expire. We'll see. + } + } + } catch (Exception e) { + throw new IllegalStateException("Unable to initialize vespa model writing to config server(s) " + + System.getProperty("configsources") + "\n" + + "Please ensure that cloudconfig_server is started on the config server node(s), " + + "and check the vespa log for configserver errors. ", e); + } + } + + /** Sets the app id and attempts to set up zookeeper. The app id must be ordered for purge to work OK. */ + private void createZooKeeperNodes() { + if (!configCurator.exists(rootPath.getAbsolute())) { + configCurator.createNode(rootPath.getAbsolute()); + } + + for (String subPath : Arrays.asList( + ConfigCurator.DEFCONFIGS_ZK_SUBPATH, + ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH, + ConfigCurator.USERAPP_ZK_SUBPATH, + ZKApplicationPackage.fileRegistryNode)) { + // TODO The replaceFirst below is hackish. + configCurator.createNode(getZooKeeperAppPath(null).getAbsolute(), subPath.replaceFirst("/", "")); + } + } + + /** + * Feeds def files and user config into ZK. + * + * @param app the application package to feed to zookeeper + */ + void feedZooKeeper(ApplicationPackage app) { + trace("Feeding application config into ZooKeeper"); + // gives lots and lots of debug output: // BasicConfigurator.configure(); + try { + trace("zk operations: " + configCurator.getNumberOfOperations()); + trace("zk operations: " + configCurator.getNumberOfOperations()); + trace("Feeding user def files into ZooKeeper"); + feedZKUserDefs(app); + trace("zk operations: " + configCurator.getNumberOfOperations()); + trace("Feeding application package into ZooKeeper"); + // TODO 1200 zk operations done in the below method + feedZKAppPkg(app); + feedSearchDefinitions(app); + feedZKUserIncludeDirs(app, app.getUserIncludeDirs()); + trace("zk operations: " + configCurator.getNumberOfOperations()); + trace("zk read operations: " + configCurator.getNumberOfReadOperations()); + trace("zk write operations: " + configCurator.getNumberOfWriteOperations()); + trace("Feeding sd from docproc bundle into ZooKeeper"); + trace("zk operations: " + configCurator.getNumberOfOperations()); + trace("Write application metadata into ZooKeeper"); + feedZKAppMetaData(app.getMetaData()); + trace("zk operations: " + configCurator.getNumberOfOperations()); + } catch (Exception e) { + throw new IllegalStateException("Unable to write vespa model to config server(s) " + System.getProperty("configsources") + "\n" + + "Please ensure that cloudconfig_server is started on the config server node(s), " + + "and check the vespa log for configserver errors. ", e); + } + } + + private void feedSearchDefinitions(ApplicationPackage app) throws IOException { + Collection<NamedReader> sds = app.getSearchDefinitions(); + if (sds.isEmpty()) { + return; + } + Path zkPath = getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.SEARCH_DEFINITIONS_DIR); + configCurator.createNode(zkPath.getAbsolute()); + // Ensures that ranking expressions and other files are also fed. + feedDirZooKeeper(app.getFile(ApplicationPackage.SEARCH_DEFINITIONS_DIR), zkPath, false); + for (NamedReader sd : sds) { + String name = sd.getName(); + Reader reader = sd.getReader(); + String data = com.yahoo.io.IOUtils.readAll(reader); + reader.close(); + configCurator.putData(zkPath.getAbsolute(), name, data); + } + } + + /** + * Puts the application package files into ZK + * + * @param app The application package to use as input. + * @throws java.io.IOException if not able to write to Zookeeper + */ + void feedZKAppPkg(ApplicationPackage app) throws IOException { + ApplicationFile.PathFilter srFilter = new ApplicationFile.PathFilter() { + @Override + public boolean accept(Path path) { + return path.getName().endsWith(ApplicationPackage.RULES_NAME_SUFFIX); + } + }; + // Copy app package files and subdirs into zk + // TODO: We should have a way of doing this which doesn't require repeating all the content + feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.SERVICES)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH)); + feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.HOSTS)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH)); + feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.DEPLOYMENT_FILE.getName())), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH)); + feedFileZooKeeper(app.getFile(Path.fromString(ApplicationPackage.VALIDATION_OVERRIDES.getName())), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH)); + + feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.TEMPLATES_DIR)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH), + true); + feedDirZooKeeper(app.getFile(ApplicationPackage.RULES_DIR), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.RULES_DIR), + srFilter, true); + feedDirZooKeeper(app.getFile(ApplicationPackage.QUERY_PROFILES_DIR), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.QUERY_PROFILES_DIR), + xmlFilter, true); + feedDirZooKeeper(app.getFile(ApplicationPackage.PAGE_TEMPLATES_DIR), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.PAGE_TEMPLATES_DIR), + xmlFilter, true); + feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.SEARCHCHAINS_DIR)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.SEARCHCHAINS_DIR), + xmlFilter, true); + feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.DOCPROCCHAINS_DIR)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.DOCPROCCHAINS_DIR), + xmlFilter, true); + feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.ROUTINGTABLES_DIR)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.ROUTINGTABLES_DIR), + xmlFilter, true); + feedDirZooKeeper(app.getFile(Path.fromString(ApplicationPackage.FILES_DIR)), + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH).append(ApplicationPackage.FILES_DIR), + true); + } + + private void feedDirZooKeeper(ApplicationFile file, Path zooKeeperAppPath, boolean recurse) throws IOException { + feedDirZooKeeper(file, zooKeeperAppPath, new ApplicationFile.PathFilter() { + @Override + public boolean accept(Path path) { + return true; + } + }, recurse); + } + + private void feedDirZooKeeper(ApplicationFile dir, Path path, ApplicationFile.PathFilter filenameFilter, boolean recurse) throws IOException { + if (!dir.isDirectory()) { + logger.log(LogLevel.FINE, dir.getPath().getAbsolute()+" is not a directory. Not feeding the files into ZooKeeper."); + return; + } + for (ApplicationFile file: listFiles(dir, filenameFilter)) { + String name = file.getPath().getName(); + if (name.startsWith(".")) continue; //.svn , .git ... + if ("CVS".equals(name)) continue; + if (file.isDirectory()) { + configCurator.createNode(path.append(name).getAbsolute()); + if (recurse) { + feedDirZooKeeper(file, path.append(name), filenameFilter, recurse); + } + } else { + feedFileZooKeeper(file, path); + } + } + } + + /** + * Like {@link ApplicationFile#listFiles(com.yahoo.config.application.api.ApplicationFile.PathFilter)} with a slightly different semantic. Never filter out directories. + */ + private List<ApplicationFile> listFiles(ApplicationFile dir, ApplicationFile.PathFilter filter) { + List<ApplicationFile> rawList = dir.listFiles(); + List<ApplicationFile> ret = new ArrayList<>(); + if (rawList != null) { + for (ApplicationFile f : rawList) { + if (f.isDirectory()) { + ret.add(f); + } else { + if (filter.accept(f.getPath())) { + ret.add(f); + } + } + } + } + return ret; + } + + private void feedFileZooKeeper(ApplicationFile file, Path zkPath) throws IOException { + if (!file.exists()) { + return; + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (InputStream inputStream = file.createInputStream()) { + IOUtils.copy(inputStream, baos); + baos.flush(); + configCurator.putData(zkPath.append(file.getPath().getName()).getAbsolute(), baos.toByteArray()); + } + } + + private void feedZKUserIncludeDirs(ApplicationPackage applicationPackage, List<String> userIncludeDirs) throws IOException { + // User defined include directories + for (String userInclude : userIncludeDirs) { + ApplicationFile dir = applicationPackage.getFile(Path.fromString(userInclude)); + final List<ApplicationFile> files = dir.listFiles(); + if (files == null || files.isEmpty()) { + configCurator.createNode(getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + userInclude).getAbsolute()); + } + feedDirZooKeeper(dir, + getZooKeeperAppPath(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + userInclude), + xmlFilter, true); + } + } + + /** + * Feeds all user-defined .def file from the application package into ZooKeeper (both into + * /defconfigs and /userdefconfigs + */ + private void feedZKUserDefs(ApplicationPackage applicationPackage) { + Map<ConfigDefinitionKey, UnparsedConfigDefinition> configDefs = applicationPackage.getAllExistingConfigDefs(); + for (Map.Entry<ConfigDefinitionKey, UnparsedConfigDefinition> entry : configDefs.entrySet()) { + ConfigDefinitionKey key = entry.getKey(); + String contents = entry.getValue().getUnparsedContent(); + feedDefToZookeeper(key.getName(), key.getNamespace(), getZooKeeperAppPath(ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH).getAbsolute(), contents); + feedDefToZookeeper(key.getName(), key.getNamespace(), getZooKeeperAppPath(ConfigCurator.DEFCONFIGS_ZK_SUBPATH).getAbsolute(), contents); + } + logger.log(LogLevel.FINE, configDefs.size() + " user config definitions"); + } + + private void feedDefToZookeeper(String name, String namespace, String path, String data) { + feedDefToZookeeper(name, namespace, "", path, com.yahoo.text.Utf8.toBytes(data)); + } + + private void feedDefToZookeeper(String name, String namespace, String version, String path, byte[] data) { + configCurator.putDefData( + ("".equals(namespace)) ? name : (namespace + "." + name), + version, + path, + data); + } + + private void feedZKFileRegistry(Version vespaVersion, FileRegistry fileRegistry) { + trace("Feeding file registry data into ZooKeeper"); + String exportedRegistry = PreGeneratedFileRegistry.exportRegistry(fileRegistry); + + configCurator.putData(getZooKeeperAppPath(null).append(ZKApplicationPackage.fileRegistryNode).getAbsolute(), + vespaVersion.toSerializedForm(), + exportedRegistry); + } + + /** + * Feeds application metadata to zookeeper. Used by vespamodel to create config + * for application metadata (used by ApplicationStatusHandler) + * + * @param metaData The application metadata. + */ + private void feedZKAppMetaData(ApplicationMetaData metaData) { + configCurator.putData(getZooKeeperAppPath(ConfigCurator.META_ZK_PATH).getAbsolute(), metaData.asJsonString()); + } + + void cleanupZooKeeper() { + trace("Exception occurred. Cleaning up ZooKeeper"); + try { + for (String subPath : Arrays.asList( + ConfigCurator.DEFCONFIGS_ZK_SUBPATH, + ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH, + ConfigCurator.USERAPP_ZK_SUBPATH)) { + configCurator.deleteRecurse(getZooKeeperAppPath(null).append(subPath).getAbsolute()); + } + } catch (Exception e) { + logger.log(LogLevel.WARNING, "Could not clean up in zookeeper"); + //Might be called in an exception handler before re-throw, so do not throw here. + } + } + + /** + * Gets a full ZK app path based on id set in Admin object + * + * + * @param trailingPath trailing part of path to be appended to ZK app path + * @return a String with the full ZK application path including trailing path, if set + */ + Path getZooKeeperAppPath(String trailingPath) { + if (trailingPath != null) { + return rootPath.append(trailingPath); + } else { + return rootPath; + } + } + + void trace(String msg) { + if (trace) { + logger.log(LogLevel.FINE, msg); + } + } + + private void feedProvisionInfo(Version version, ProvisionInfo info) throws IOException { + byte[] json = info.toJson(); + configCurator.putData(rootPath.append(ZKApplicationPackage.allocatedHostsNode).append(version.toSerializedForm()).getAbsolute(), json); + } + + public void feedZKFileRegistries(Map<Version, FileRegistry> fileRegistryMap) throws IOException { + for (Map.Entry<Version, FileRegistry> versionFileRegistryEntry : fileRegistryMap.entrySet()) { + feedZKFileRegistry(versionFileRegistryEntry.getKey(), versionFileRegistryEntry.getValue()); + } + } + + public void feedProvisionInfos(Map<Version, ProvisionInfo> provisionInfoMap) throws IOException { + for (Map.Entry<Version, ProvisionInfo> versionProvisionInfoEntry : provisionInfoMap.entrySet()) { + feedProvisionInfo(versionProvisionInfoEntry.getKey(), versionProvisionInfoEntry.getValue()); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java new file mode 100644 index 00000000000..3a92c4a5ebe --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/deploy/ZooKeeperDeployer.java @@ -0,0 +1,45 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.deploy; + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.Version; + +import java.io.IOException; +import java.util.Map; + +/** + * Interface for initializing zookeeper and deploying an application package to zookeeper. + * Initialize must be called before each deploy. + * + * @author lulf + * @since 5.1 + */ +public class ZooKeeperDeployer { + + private final ZooKeeperClient zooKeeperClient; + + public ZooKeeperDeployer(ZooKeeperClient client) { + this.zooKeeperClient = client; + } + + /** + * Deploys an application package to zookeeper. initialize() must be called before calling this method. + * + * @param applicationPackage The application package to persist. + * @param fileRegistryMap The file registries to persist. + * @param provisionInfoMap The provisioning infos to persist. + * @throws IOException if deploying fails + */ + public void deploy(ApplicationPackage applicationPackage, Map<Version, FileRegistry> fileRegistryMap, Map<Version, ProvisionInfo> provisionInfoMap) throws IOException { + zooKeeperClient.setupZooKeeper(); + zooKeeperClient.feedZooKeeper(applicationPackage); + zooKeeperClient.feedZKFileRegistries(fileRegistryMap); + zooKeeperClient.feedProvisionInfos(provisionInfoMap); + } + + public void cleanup() { + zooKeeperClient.cleanupZooKeeper(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java new file mode 100644 index 00000000000..2f9c13587fe --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBHandler.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.config.FileReference; +import com.yahoo.config.model.api.FileDistribution; +import com.yahoo.vespa.filedistribution.FileDistributionManager; + +import java.util.*; + +/** + * Implements invoker of filedistribution using manager with JNI. + * + * @author tonytv + * @author lulf + * @since 5.1.14 + */ +public class FileDBHandler implements FileDistribution { + private final FileDistributionManager manager; + + public FileDBHandler(FileDistributionManager manager) { + this.manager = manager; + } + + @Override + public void sendDeployedFiles(String hostName, Set<FileReference> fileReferences) { + List<String> referencesAsString = new ArrayList<>(); + for (FileReference reference : fileReferences) { + referencesAsString.add(reference.value()); + } + manager.setDeployedFiles(hostName, referencesAsString); + } + + @Override + public void limitSendingOfDeployedFilesTo(Collection<String> hostNames) { + manager.limitSendingOfDeployedFilesTo(hostNames); + } + + @Override + public void removeDeploymentsThatHaveDifferentApplicationId(Collection<String> targetHostnames) { + manager.removeDeploymentsThatHaveDifferentApplicationId(targetHostnames); + } + + @Override + public void reloadDeployFileDistributor() { + manager.reloadDeployFileDistributor(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java new file mode 100644 index 00000000000..58d651ae33a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDBRegistry.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.config.FileReference; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.net.HostName; +import com.yahoo.vespa.filedistribution.FileDistributionManager; +import com.yahoo.config.model.application.provider.FileReferenceCreator; + +import java.util.*; + +/** + * @author tonytv + */ +public class FileDBRegistry implements FileRegistry { + + private final FileDistributionManager manager; + private List<Entry> entries = new ArrayList<>(); + private final Map<String, FileReference> fileReferenceCache = new HashMap<>(); + + public FileDBRegistry(FileDistributionManager manager) { + this.manager = manager; + } + + @Override + public synchronized FileReference addFile(String relativePath) { + Optional<FileReference> cachedReference = Optional.ofNullable(fileReferenceCache.get(relativePath)); + return cachedReference.orElseGet(() -> { + FileReference newRef = FileReferenceCreator.create(manager.addFile(relativePath)); + entries.add(new Entry(relativePath, newRef)); + fileReferenceCache.put(relativePath, newRef); + return newRef; + }); + } + + @Override + public String fileSourceHost() { + return HostName.getLocalhost(); + } + + @Override + public synchronized List<Entry> export() { + return entries; + } + + @Override + public Set<String> allRelativePaths() { + Set<String> ret = new HashSet<>(); + for (Entry entry : entries) { + ret.add(entry.relativePath); + } + return ret; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java new file mode 100644 index 00000000000..1082e0a7f6c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionLock.java @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.recipes.CuratorLock; +import com.yahoo.vespa.curator.recipes.CuratorLockException; + +import java.time.Clock; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Global filedistribution lock to ensure only one configserver may work on filedistribution. + * The implementation uses a combination of a {@link java.util.concurrent.locks.ReentrantLock} and + * a {@link CuratorLock} to ensure both mutual exclusion within the JVM and + * across JVMs via ZooKeeper. + * + * @author lulf + */ +public class FileDistributionLock implements Lock { + private final Lock processLock; + private final CuratorLock curatorLock; + + public FileDistributionLock(Curator curator, String zkPath) { + this.processLock = new ReentrantLock(); + this.curatorLock = new CuratorLock(curator, zkPath); + } + + @Override + public void lock() { + processLock.lock(); + try { + curatorLock.lock(); + } catch (CuratorLockException e) { + processLock.unlock(); + throw e; + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + throw new UnsupportedOperationException(); + + } + + @Override + public boolean tryLock() { + if (processLock.tryLock()) { + if (curatorLock.tryLock()) { + return true; + } else { + processLock.unlock(); + return false; + } + } else { + return false; + } + } + + @Override + public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { + TimeoutBudget budget = new TimeoutBudget(Clock.systemUTC(), Duration.ofMillis(unit.toMillis(timeout))); + if (processLock.tryLock(budget.timeLeft().toMillis(), TimeUnit.MILLISECONDS)) { + if (curatorLock.tryLock(budget.timeLeft().toMillis(), TimeUnit.MILLISECONDS)) { + return true; + } else { + processLock.unlock(); + return false; + } + } else { + return false; + } + } + + @Override + public void unlock() { + try { + curatorLock.unlock(); + } finally { + processLock.unlock(); + } + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } +} + diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java new file mode 100644 index 00000000000..2453381131d --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/FileDistributionProvider.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.config.model.api.FileDistribution; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.vespa.filedistribution.FileDistributionManager; + +import java.io.File; +import java.util.concurrent.locks.Lock; + +/** + * Provides file distribution registry and invoker. + * + * @author lulf + * @since 5.1.14 + */ +public class FileDistributionProvider { + + private final FileRegistry fileRegistry; + private final FileDistribution fileDistribution; + + public FileDistributionProvider(File applicationDir, String zooKeepersSpec, String applicationId, Lock fileDistributionLock) { + ensureDirExists(FileDistribution.getDefaultFileDBPath()); + final FileDistributionManager manager = new FileDistributionManager( + FileDistribution.getDefaultFileDBPath(), + applicationDir, + zooKeepersSpec, + applicationId, + fileDistributionLock); + this.fileDistribution = new FileDBHandler(manager); + this.fileRegistry = new FileDBRegistry(manager); + } + + public FileDistributionProvider(FileRegistry fileRegistry, FileDistribution fileDistribution) { + this.fileRegistry = fileRegistry; + this.fileDistribution = fileDistribution; + } + + public FileRegistry getFileRegistry() { + return fileRegistry; + } + + public FileDistribution getFileDistribution() { + return fileDistribution; + } + + private void ensureDirExists(File dir) { + if (!dir.exists()) { + boolean success = dir.mkdirs(); + if (!success) + throw new RuntimeException("Could not create directory " + dir.getPath()); + } + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java new file mode 100644 index 00000000000..b61a1c8d9bc --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDBHandler.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.config.FileReference; +import com.yahoo.config.model.api.FileDistribution; + +import java.util.Collection; +import java.util.Set; + +/** + * @author lulf + * @since 5.1 + */ +public class MockFileDBHandler implements FileDistribution { + public int sendDeployedFilesCalled = 0; + public int reloadDeployFileDistributorCalled = 0; + public int limitSendingOfDeployedFilesToCalled = 0; + public int removeDeploymentsThatHaveDifferentApplicationIdCalled = 0; + + @Override + public void sendDeployedFiles(String hostName, Set<FileReference> fileReferences) { + sendDeployedFilesCalled++; + } + + @Override + public void reloadDeployFileDistributor() { + reloadDeployFileDistributorCalled++; + } + + @Override + public void limitSendingOfDeployedFilesTo(Collection<String> hostNames) { + limitSendingOfDeployedFilesToCalled++; + } + + @Override + public void removeDeploymentsThatHaveDifferentApplicationId(Collection<String> targetHostnames) { + removeDeploymentsThatHaveDifferentApplicationIdCalled++; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java new file mode 100644 index 00000000000..588d7c259b6 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/filedistribution/MockFileDistributionProvider.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.filedistribution; + +import com.yahoo.config.model.application.provider.MockFileRegistry; + +/** + * @author lulf + * @since 5.1 + */ +public class MockFileDistributionProvider extends FileDistributionProvider { + + public MockFileDistributionProvider() { + super(new MockFileRegistry(), new MockFileDBHandler()); + } + + public MockFileDBHandler getMockFileDBHandler() { + return (MockFileDBHandler) getFileDistribution(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java new file mode 100644 index 00000000000..2af55c343a1 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/BadRequestException.java @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +/** + * Exception that will create a http response with BAD_REQUEST response code (400) + * + * @author musum + * @since 5.1.17 + */ +public class BadRequestException extends RuntimeException { + + public BadRequestException(String message) { + super(message); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java new file mode 100644 index 00000000000..c2d255e1630 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentHandler.java @@ -0,0 +1,108 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Slime; + +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Requests for content and content status, both for prepared and active sessions, + * are handled by this class. + * + * @author musum + * @since 5.1.15 + */ +public class ContentHandler { + + public HttpResponse get(ContentRequest request) { + ContentRequest.ReturnType returnType = request.getReturnType(); + String urlBase = request.getUrlBase("/content/"); + if (ContentRequest.ReturnType.STATUS.equals(returnType)) { + return status(request, urlBase); + } else { + return content(request, urlBase); + } + } + + public HttpResponse put(ContentRequest request) { + ApplicationFile file = request.getFile(); + if (request.getPath().endsWith("/")) { + createDirectory(request, file); + } else { + createFile(request, file); + } + return createResponse(request); + } + + public HttpResponse delete(ContentRequest request) { + ApplicationFile file = request.getExistingFile(); + deleteFile(file); + return createResponse(request); + } + + private HttpResponse content(ContentRequest request, String urlBase) { + ApplicationFile file = request.getExistingFile(); + if (file.isDirectory()) { + return new SessionContentListResponse(urlBase, listSortedFiles(file, request.getPath(), request.isRecursive())); + } + return new SessionContentReadResponse(file); + } + + private HttpResponse status(ContentRequest request, String urlBase) { + ApplicationFile file = request.getFile(); + if (file.isDirectory()) { + return new SessionContentStatusListResponse(urlBase, listSortedFiles(file, request.getPath(), request.isRecursive())); + } + return new SessionContentStatusResponse(file, urlBase); + } + + private static List<ApplicationFile> listSortedFiles(ApplicationFile file, String path, boolean recursive) { + if (!path.isEmpty() && !path.endsWith("/")) { + return Arrays.asList(file); + } + List<ApplicationFile> files = file.listFiles(recursive); + Collections.sort(files); + return files; + } + + private void createFile(ContentRequest request, ApplicationFile file) { + if (!request.hasRequestBody()) { + throw new BadRequestException("Request must contain body when creating a file"); + } + try { + file.writeFile(new InputStreamReader(request.getData())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("StatementWithEmptyBody") + private void createDirectory(ContentRequest request, ApplicationFile file) { + if (request.hasRequestBody()) { + // TODO: Enable when we have a good way to check if request contains a body + // return new HttpErrorResponse(HttpResponse.Status.BAD_REQUEST, "Request should not contain a body when creating directories"); + } + file.createDirectory(); + } + + private void deleteFile(ApplicationFile file) { + try { + file.delete(); + } catch (RuntimeException e) { + throw new BadRequestException("File '" + file.getPath() + "' is not an empty directory"); + } + } + + private HttpResponse createResponse(ContentRequest request) { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + root.setString("prepared", request.getUrlBase("/prepared")); + return new SessionResponse(slime, root); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java new file mode 100644 index 00000000000..d71987449bf --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/ContentRequest.java @@ -0,0 +1,95 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.session.LocalSession; + +import java.io.InputStream; + +/** + * Represents a {@link ContentRequest}, and contains common functionality for content requests for all content handlers. + * + * @author lulf + * @since 5.3 + */ +public abstract class ContentRequest { + private static final String RETURN_QUERY_PROPERTY = "return"; + + enum ReturnType {CONTENT, STATUS} + + private final long sessionId; + private final String path; + private final ApplicationFile file; + private final HttpRequest request; + + protected ContentRequest(HttpRequest request, LocalSession session) { + this.request = request; + this.sessionId = session.getSessionId(); + this.path = getContentPath(request); + this.file = session.getApplicationFile(Path.fromString(path), getApplicationFileMode(request.getMethod())); + } + + private LocalSession.Mode getApplicationFileMode(com.yahoo.jdisc.http.HttpRequest.Method method) { + switch (method) { + case GET: + case OPTIONS: + return LocalSession.Mode.READ; + default: + return LocalSession.Mode.WRITE; + } + } + + ReturnType getReturnType() { + if (request.hasProperty(RETURN_QUERY_PROPERTY)) { + String type = request.getProperty(RETURN_QUERY_PROPERTY); + switch (type) { + case "content": + return ReturnType.CONTENT; + case "status": + return ReturnType.STATUS; + default: + throw new BadRequestException("return=" + type + " is an illegal argument. Only " + + ReturnType.CONTENT.name() + " and " + ReturnType.STATUS.name() + " are allowed"); + } + } else { + return ReturnType.CONTENT; + } + } + + protected abstract String getPathPrefix(); + protected abstract String getContentPath(HttpRequest request); + + String getUrlBase(String appendStr) { + return Utils.getUrlBase(request, getPathPrefix() + appendStr); + } + + boolean isRecursive() { + return request.getBooleanProperty("recursive"); + } + + boolean hasRequestBody() { + return request.getData() != null; + } + + InputStream getData() { + return request.getData(); + } + + + String getPath() { + return path; + } + + ApplicationFile getFile() { + return file; + } + + ApplicationFile getExistingFile() { + if (!file.exists()) { + throw new NotFoundException("Session " + sessionId + " does not contain a file '" + path + "'"); + } + return file; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java new file mode 100644 index 00000000000..04e286ac96f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigRequest.java @@ -0,0 +1,197 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import javax.annotation.Nullable; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.yahoo.collections.Tuple2; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.GetConfigRequest; +import com.yahoo.vespa.config.protocol.DefContent; +import com.yahoo.vespa.config.protocol.VespaVersion; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.v2.HttpConfigRequests; +import com.yahoo.vespa.config.server.http.v2.TenantRequest; +import com.yahoo.vespa.config.util.ConfigUtils; + +/** + * A request to get config, bound to tenant and app id. Used by both v1 and v2 of the config REST API. + * + * @author lulf + * @since 5.1 + */ +public class HttpConfigRequest implements GetConfigRequest, TenantRequest { + private static final String DEFAULT_TENANT = "default"; + private static final String HTTP_PROPERTY_NOCACHE = "noCache"; + private final ConfigKey<?> key; + private final ApplicationId appId; + private final boolean noCache; + + private HttpConfigRequest(ConfigKey<?> key, ApplicationId appId, boolean noCache) { + this.key = key; + this.appId = appId; + this.noCache = noCache; + } + + private static ConfigKey<?> fromRequestV1(HttpRequest req) { + BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*"); // see jdisc-bindings.cfg + String conf = bm.group(2); // The port number is implicitly 1, it seems + String cId; + String cName; + String cNamespace; + if (bm.groupCount() >= 4) { + cId = bm.group(3); + } else { + cId = ""; + } + Tuple2<String, String> nns = nameAndNamespace(conf); + cName = nns.first; + cNamespace = nns.second; + return new ConfigKey<>(cName, cId, cNamespace); + } + + public static HttpConfigRequest createFromRequestV1(HttpRequest req) { + return new HttpConfigRequest(fromRequestV1(req), ApplicationId.defaultId(), req.getBooleanProperty(HTTP_PROPERTY_NOCACHE)); + } + + public static HttpConfigRequest createFromRequestV2(HttpRequest req) { + // Four bindings for this: with full app id or only name, with and without config id (like v1) + BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req, + "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*", + "http://*/config/v2/tenant/*/application/*/*/*"); + if (bm.groupCount() > 6) return createFromRequestV2FullAppId(req, bm); + return createFromRequestV2SimpleAppId(req, bm); + } + + // The URL pattern with only tenant and application given + private static HttpConfigRequest createFromRequestV2SimpleAppId(HttpRequest req, BindingMatch<?> bm) { + String cId; + String cName; + String cNamespace; + TenantName tenant = TenantName.from(bm.group(2)); + ApplicationName application = ApplicationName.from(bm.group(3)); + String conf = bm.group(4); + if (bm.groupCount() >= 6) { + cId = bm.group(5); + } else { + cId = ""; + } + Tuple2<String, String> nns = nameAndNamespace(conf); + cName = nns.first; + cNamespace = nns.second; + return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace), + new ApplicationId.Builder().applicationName(application).tenant(tenant).build(), + req.getBooleanProperty(HTTP_PROPERTY_NOCACHE)); + } + + // The URL pattern with full app id given + private static HttpConfigRequest createFromRequestV2FullAppId(HttpRequest req, BindingMatch<?> bm) { + String cId; + String cName; + String cNamespace; + String tenant = bm.group(2); + String application = bm.group(3); + String environment = bm.group(4); + String region = bm.group(5); + String instance = bm.group(6); + String conf = bm.group(7); + if (bm.groupCount() >= 9) { + cId = bm.group(8); + } else { + cId = ""; + } + Tuple2<String, String> nns = nameAndNamespace(conf); + cName = nns.first; + cNamespace = nns.second; + + ApplicationId appId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName(application) + .instanceName(instance) + .build(); + return new HttpConfigRequest(new ConfigKey<>(cName, cId, cNamespace), appId, req.getBooleanProperty(HTTP_PROPERTY_NOCACHE)); + } + + /** + * Throws an exception if bad config or config id + * + * @param requestKey a {@link com.yahoo.vespa.config.ConfigKey} + * @param requestHandler a {@link RequestHandler} + * @param appId appId + */ + public static void validateRequestKey(ConfigKey<?> requestKey, RequestHandler requestHandler, ApplicationId appId) { + Set<ConfigKey<?>> allConfigsProduced = requestHandler.allConfigsProduced(appId, Optional.empty()); + if (allConfigsProduced.isEmpty()) { + // This will happen if the configserver is starting up, but has not built a config model + throwModelNotReady(); + } + if (configNameNotFound(requestKey, allConfigsProduced)) { + throw new NotFoundException("No such config: " + requestKey.getNamespace() + "." + requestKey.getName()); + } + if (configIdNotFound(requestHandler, requestKey, appId)) { + throw new NotFoundException("No such config id: " + requestKey.getConfigId()); + } + } + + public static void throwModelNotReady() { + throw new NotFoundException("Config not available, verify that an application package has been deployed and activated."); + } + + /** + * If the given config is produced by the model at all + * + * @return ok or not + */ + private static boolean configNameNotFound(final ConfigKey<?> requestKey, Set<ConfigKey<?>> allConfigsProduced) { + return !Iterables.any(allConfigsProduced, new Predicate<ConfigKey<?>>() { + @Override + public boolean apply(@Nullable ConfigKey<?> k) { + return k.getName().equals(requestKey.getName()) && k.getNamespace().equals(requestKey.getNamespace()); + } + }); + } + + private static boolean configIdNotFound(RequestHandler requestHandler, ConfigKey<?> requestKey, ApplicationId appId) { + return !requestHandler.allConfigIds(appId, Optional.empty()).contains(requestKey.getConfigId()); + } + + public static Tuple2<String, String> nameAndNamespace(String nsDotName) { + Tuple2<String, String> ret = ConfigUtils.getNameAndNamespaceFromString(nsDotName); + if ("".equals(ret.second)) throw new IllegalArgumentException("Illegal config, must be of form namespace.name."); + return ret; + } + + @Override + public ConfigKey<?> getConfigKey() { + return key; + } + + @Override + public DefContent getDefContent() { + return DefContent.fromList(Collections.<String>emptyList()); + } + + @Override + public Optional<VespaVersion> getVespaVersion() { + return Optional.empty(); + } + + @Override + public ApplicationId getApplicationId() { + return appId; + } + + public boolean noCache() { + return noCache; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java new file mode 100644 index 00000000000..10f5243cfdb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpConfigResponse.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.vespa.config.protocol.CompressionType; +import com.yahoo.vespa.config.protocol.ConfigResponse; + +import java.io.IOException; +import java.io.OutputStream; + +import static com.yahoo.jdisc.http.HttpResponse.Status.OK; + +/** + * HTTP getConfig response + * + * @author lulf + * @since 5.1 + */ +public class HttpConfigResponse extends HttpResponse { + public static final String JSON_CONTENT_TYPE = "application/json"; + private final ConfigResponse config; + + private HttpConfigResponse(ConfigResponse config) { + super(OK); + this.config = config; + } + + public static HttpConfigResponse createFromConfig(ConfigResponse config) { + return new HttpConfigResponse(config); + } + + @Override + public void render(OutputStream outputStream) throws IOException { + config.serialize(outputStream, CompressionType.UNCOMPRESSED); + } + + @Override + public String getContentType() { + return JSON_CONTENT_TYPE; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java new file mode 100644 index 00000000000..b5886992f10 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpErrorResponse.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Logger; + +import static com.yahoo.jdisc.Response.Status.*; + +/** + * @author lulf + * @since 5.1 + */ +public class HttpErrorResponse extends HttpResponse { + Logger log = Logger.getLogger(HttpErrorResponse.class.getName()); + private final Slime slime = new Slime(); + + public HttpErrorResponse(int code, final String errorType, final String msg) { + super(code); + final Cursor root = slime.setObject(); + root.setString("error-code", errorType); + root.setString("message", msg); + if (code != 200) { + log.log(LogLevel.INFO, "Returning response with response code " + code + ", error-code:" + errorType + ", message=" + msg); + } + } + + public enum errorCodes { + NOT_FOUND, + BAD_REQUEST, + METHOD_NOT_ALLOWED, + INTERNAL_SERVER_ERROR, + INVALID_APPLICATION_PACKAGE, + UNKNOWN_VESPA_VERSION, + OUT_OF_CAPACITY + } + + public static HttpErrorResponse notFoundError(String msg) { + return new HttpErrorResponse(NOT_FOUND, errorCodes.NOT_FOUND.name(), msg); + } + + public static HttpErrorResponse internalServerError(String msg) { + return new HttpErrorResponse(INTERNAL_SERVER_ERROR, errorCodes.INTERNAL_SERVER_ERROR.name(), msg); + } + + public static HttpErrorResponse invalidApplicationPackage(String msg) { + return new HttpErrorResponse(BAD_REQUEST, errorCodes.INVALID_APPLICATION_PACKAGE.name(), msg); + } + + public static HttpErrorResponse outOfCapacity(String msg) { + return new HttpErrorResponse(BAD_REQUEST, errorCodes.OUT_OF_CAPACITY.name(), msg); + } + + public static HttpErrorResponse badRequest(String msg) { + return new HttpErrorResponse(BAD_REQUEST, errorCodes.BAD_REQUEST.name(), msg); + } + + public static HttpErrorResponse methodNotAllowed(String msg) { + return new HttpErrorResponse(METHOD_NOT_ALLOWED, errorCodes.METHOD_NOT_ALLOWED.name(), msg); + } + + public static HttpResponse unknownVespaVersion(String message) { + return new HttpErrorResponse(BAD_REQUEST, errorCodes.UNKNOWN_VESPA_VERSION.name(), message); + } + + @Override + public void render(OutputStream stream) throws IOException { + new JsonFormat(true).encode(stream, slime); + } + + //@Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java new file mode 100644 index 00000000000..50a1a3877ef --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpGetConfigHandler.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.config.provision.ApplicationId; + +import java.util.Optional; +import java.util.concurrent.Executor; + +/** + * HTTP handler for a v2 getConfig operation + * + * @author lulf + * @since 5.1 + */ +public class HttpGetConfigHandler extends HttpHandler { + private final RequestHandler requestHandler; + + public HttpGetConfigHandler(Executor executor, RequestHandler requestHandler, AccessLog accessLog) { + super(executor, accessLog); + this.requestHandler = requestHandler; + } + + @Inject + public HttpGetConfigHandler(Executor executor, Tenants tenants, AccessLog accesslog) { + this(executor, tenants.defaultTenant().getRequestHandler(), accesslog); + } + + @Override + public HttpResponse handleGET(HttpRequest req) { + HttpConfigRequest request = HttpConfigRequest.createFromRequestV1(req); + HttpConfigRequest.validateRequestKey(request.getConfigKey(), requestHandler, ApplicationId.defaultId()); + return HttpConfigResponse.createFromConfig(resolveConfig(request)); + } + + private ConfigResponse resolveConfig(HttpConfigRequest request) { + log.log(LogLevel.DEBUG, "nocache=" + request.noCache()); + ConfigResponse config = requestHandler.resolveConfig(ApplicationId.defaultId(), request, Optional.empty()); + if (config == null) HttpConfigRequest.throwModelNotReady(); + return config; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java new file mode 100644 index 00000000000..7eb6f9c2271 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpHandler.java @@ -0,0 +1,131 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.jdisc.LoggingRequestHandler; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.log.LogLevel; +import com.yahoo.config.provision.OutOfCapacityException; +import com.yahoo.yolean.Exceptions; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.concurrent.Executor; + +/** + * Super class for http handlers, that takes care of checking valid + * methods for a request. Handlers should subclass this method and + * implement the handleMETHOD methods that it supports. + * + * @author musum + * @since 5.1.14 + */ +public class HttpHandler extends LoggingRequestHandler { + + public HttpHandler(Executor executor, AccessLog accessLog) { + super(executor, accessLog); + } + + @Override + public HttpResponse handle(HttpRequest request) { + log.log(LogLevel.DEBUG, request.getMethod() + " " + request.getUri().toString()); + try { + switch (request.getMethod()) { + case POST: + return handlePOST(request); + case GET: + return handleGET(request); + case PUT: + return handlePUT(request); + case DELETE: + return handleDELETE(request); + default: + return createErrorResponse(request.getMethod()); + } + } catch (NotFoundException e) { + return HttpErrorResponse.notFoundError(getMessage(e, request)); + } catch (BadRequestException e) { + return HttpErrorResponse.badRequest(getMessage(e, request)); + } catch (IllegalArgumentException | IllegalStateException e) { + return HttpErrorResponse.badRequest(getMessage(e, request)); + } catch (InvalidApplicationException e) { + return HttpErrorResponse.invalidApplicationPackage(getMessage(e, request)); + } catch (OutOfCapacityException e) { + return HttpErrorResponse.outOfCapacity(getMessage(e, request)); + } catch (InternalServerException e) { + return HttpErrorResponse.internalServerError(getMessage(e, request)); + } catch (UnknownVespaVersionException e) { + return HttpErrorResponse.unknownVespaVersion(getMessage(e, request)); + } catch (Exception e) { + e.printStackTrace(); + return HttpErrorResponse.internalServerError(getMessage(e, request)); + } + } + + private String getMessage(Exception e, HttpRequest request) { + String message; + if (request.getBooleanProperty("debug")) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + message = sw.toString(); + } else { + message = Exceptions.toMessageString(e); + } + return message; + } + + /** + * Default implementation of handler for GET requests. Returns an error response. + * Override this method to handle GET requests. + * + * @param request a {@link HttpRequest} + * @return an error response with response code 405 + */ + protected HttpResponse handleGET(HttpRequest request) { + return createErrorResponse(request.getMethod()); + } + + /** + * Default implementation of handler for POST requests. Returns an error response. + * Override this method to handle POST requests. + * + * @param request a {@link HttpRequest} + * @return an error response with response code 405 + */ + protected HttpResponse handlePOST(HttpRequest request) { + return createErrorResponse(request.getMethod()); + } + + /** + * Default implementation of handler for PUT requests. Returns an error response. + * Override this method to handle POST requests. + * + * @param request a {@link HttpRequest} + * @return an error response with response code 405 + */ + protected HttpResponse handlePUT(HttpRequest request) { + return createErrorResponse(request.getMethod()); + } + + /** + * Default implementation of handler for DELETE requests. Returns an error response. + * Override this method to handle DELETE requests. + * + * @param request a {@link HttpRequest} + * @return an error response with response code 405 + */ + protected HttpResponse handleDELETE(HttpRequest request) { + return createErrorResponse(request.getMethod()); + } + + /** + * Creates error response when method is not handled + * + * @return an error response with response code 405 + */ + private HttpResponse createErrorResponse(com.yahoo.jdisc.http.HttpRequest.Method method) { + return HttpErrorResponse.methodNotAllowed("Method '" + method + "' is not supported"); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java new file mode 100644 index 00000000000..a6e2c5bf050 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListConfigsHandler.java @@ -0,0 +1,128 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.config.provision.ApplicationId; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.*; +import java.util.concurrent.Executor; + +import static com.yahoo.jdisc.http.HttpResponse.Status.*; + + +/** + * Handler for a list configs operation. Lists all configs in model. + * + * @author vegardh + * @since 5.1.11 + */ +public class HttpListConfigsHandler extends HttpHandler { + static final String RECURSIVE_QUERY_PROPERTY = "recursive"; + private final RequestHandler requestHandler; + + @Inject + public HttpListConfigsHandler(Executor executor, AccessLog accessLog, Tenants tenants) { + this(executor, accessLog, tenants.defaultTenant().getRequestHandler()); + } + + public HttpListConfigsHandler(Executor executor, AccessLog accessLog, RequestHandler requestHandler) { + super(executor, accessLog); + this.requestHandler = requestHandler; + } + + @Override + public HttpResponse handleGET(HttpRequest req) { + boolean recursive = req.getBooleanProperty(RECURSIVE_QUERY_PROPERTY); + Set<ConfigKey<?>> configs = requestHandler.listConfigs(ApplicationId.defaultId(), Optional.empty(), recursive); + String urlBase = Utils.getUrlBase(req, "/config/v1/"); + Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(ApplicationId.defaultId(), Optional.empty()); + return new ListConfigsResponse(configs, allConfigs, urlBase, recursive); + } + + static class ListConfigsResponse extends HttpResponse { + private final List<ConfigKey<?>> configs; + private final Set<ConfigKey<?>> allConfigs; + private final String urlBase; + private final boolean recursive; + + /** + * New list response + * + * @param configs the configs to include in the list + * @param urlBase for example "http://foo.com:19071/config/v1/ (configs are appended to the listed URLs based on configs list) + * @param recursive list recursively + */ + public ListConfigsResponse(Set<ConfigKey<?>> configs, Set<ConfigKey<?>> allConfigs, String urlBase, boolean recursive) { + super(OK); + this.configs = new ArrayList<>(configs); + Collections.sort(this.configs); + this.allConfigs = allConfigs; + this.urlBase = urlBase; + this.recursive = recursive; + } + + /** + * The listing URL for this config in this service + * + * @param key config key + * @param rec recursive + * @return url + */ + String toUrl(ConfigKey<?> key, boolean rec) { + return urlBase + key.getNamespace() + "." + key.getName() + "/" + key.getConfigId() + (rec ? "" : "/"); + } + + @Override + public void render(OutputStream outputStream) throws IOException { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + Cursor array; + if (!recursive) { + array = root.setArray("children"); + for (ConfigKey<?> key : keysThatHaveAChildWithSameName(configs, allConfigs)) { + array.addString(toUrl(key, false)); + } + } + array = root.setArray("configs"); + for (ConfigKey<?> key : configs) { + array.addString(toUrl(key, true)); + } + new JsonFormat(true).encode(outputStream, slime); + } + + static Set<ConfigKey<?>> keysThatHaveAChildWithSameName(Collection<ConfigKey<?>> keys, Set<ConfigKey<?>> allConfigs) { + Set<ConfigKey<?>> ret = new LinkedHashSet<>(); + for (ConfigKey<?> k : keys) { + if (ListConfigsResponse.hasAChild(k, allConfigs)) ret.add(k); + } + return ret; + } + + static boolean hasAChild(ConfigKey<?> key, Set<ConfigKey<?>> keys) { + if ("".equals(key.getConfigId())) return false; + for (ConfigKey<?> k : keys) { + if (!k.getName().equals(key.getName())) continue; + if ("".equals(k.getConfigId())) continue; + if (k.getConfigId().equals(key.getConfigId())) continue; + if (k.getConfigId().startsWith(key.getConfigId())) return true; + } + return false; + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java new file mode 100644 index 00000000000..31d6e84860d --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/HttpListNamedConfigsHandler.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.google.inject.Inject; +import com.yahoo.collections.Tuple2; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.config.provision.ApplicationId; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Executor; + +/** + * Handler for a list configs of given name operation. Lists all configs in model for a given config name. + * + * @author vegardh + * @since 5.1.11 + */ +public class HttpListNamedConfigsHandler extends HttpHandler { + private final RequestHandler requestHandler; + + public HttpListNamedConfigsHandler(Executor executor, RequestHandler requestHandler, AccessLog accessLog) { + super(executor, accessLog); + this.requestHandler = requestHandler; + } + + @Inject + public HttpListNamedConfigsHandler(Executor executor, Tenants tenants, AccessLog accessLog) { + this(executor, tenants.defaultTenant().getRequestHandler(), accessLog); + } + + @Override + public HttpResponse handleGET(HttpRequest req) { + boolean recursive = req.getBooleanProperty(HttpListConfigsHandler.RECURSIVE_QUERY_PROPERTY); + ConfigKey<?> listKey = parseReqToKey(req); + HttpConfigRequest.validateRequestKey(listKey, requestHandler, ApplicationId.defaultId()); + Set<ConfigKey<?>> configs = requestHandler.listNamedConfigs(ApplicationId.defaultId(), Optional.empty(), listKey, recursive); + String urlBase = Utils.getUrlBase(req, "/config/v1/"); + return new HttpListConfigsHandler.ListConfigsResponse(configs, requestHandler.allConfigsProduced(ApplicationId.defaultId(), Optional.empty()), urlBase, recursive); + } + + private ConfigKey<?> parseReqToKey(HttpRequest req) { + BindingMatch<?> bm = Utils.getBindingMatch(req, "http://*/config/v1/*/*"); + String config = bm.group(2); // See jdisc-bindings.cfg. The port number is implicitly 1, it seems. + Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(config); + String name = nns.first; + String namespace = nns.second; + String idSegment = ""; + if (bm.groupCount() == 4) { + idSegment = bm.group(3); + } + return new ConfigKey<>(name, idSegment, namespace); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java new file mode 100644 index 00000000000..240f5814652 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InternalServerException.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import java.io.IOException; + +/** + * Exception that will create a http response with INTERNAL_SERVER_ERROR response code (500) + * + * @author musum + * @since 5.1.17 + */ +public class InternalServerException extends RuntimeException { + + public InternalServerException(String message) { + super(message); + } + + public InternalServerException(String message, Exception e) { + super(message, e); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java new file mode 100644 index 00000000000..ba8f034777a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/InvalidApplicationException.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +/** + * @author musum + */ +public class InvalidApplicationException extends RuntimeException { + + public InvalidApplicationException(String message) { + super(message); + } + + public InvalidApplicationException(String message, Throwable e) { + super(message, e); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java new file mode 100644 index 00000000000..ae99481ccfa --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/JSONResponse.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Response that contains some utility stuff for rendering json. + * + * @author lulf + * @since 5.3 + */ +public class JSONResponse extends HttpResponse { + private final Slime slime = new Slime(); + protected final Cursor object; + public JSONResponse(int status) { + super(status); + this.object = slime.setObject(); + } + + @Override + public void render(OutputStream outputStream) throws IOException { + new JsonFormat(true).encode(outputStream, slime); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java new file mode 100644 index 00000000000..327e792134a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/NotFoundException.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +/** + * Exception that will create a http response with NOT_FOUND response code (404) + * + * @author musum + * @since 5.1.17 + */ +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } +} + diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java new file mode 100644 index 00000000000..53133b49c8a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionActiveHandlerBase.java @@ -0,0 +1,55 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.provision.Provisioner; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.vespa.config.server.ActivateLock; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.deploy.Deployer; +import com.yahoo.vespa.config.server.deploy.Deployment; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; + +import java.util.Optional; +import java.util.concurrent.Executor; + +/** + * @author lulf + */ +public class SessionActiveHandlerBase extends SessionHandler { + + public SessionActiveHandlerBase(Executor executor, AccessLog accessLog) { + super(executor, accessLog); + } + + protected void activate(HttpRequest request, + LocalSessionRepo localSessionRepo, + ActivateLock activateLock, + TimeoutBudget timeoutBudget, + Optional<Provisioner> hostProvisioner, + LocalSession localSession) { + // TODO: Use an injected deployer from the callers of this instead + // TODO: And then get rid of the activateLock and localSessionRepo arguments in deployFromPreparedSession + Deployer deployer = new Deployer(null, HostProvisionerProvider.from(hostProvisioner), null, null); + Deployment deployment = deployer.deployFromPreparedSession(localSession, activateLock, localSessionRepo, timeoutBudget.timeLeft()); + deployment.setIgnoreLockFailure(shouldIgnoreLockFailure(request)); + deployment.setIgnoreSessionStaleFailure(shouldIgnoreSessionStaleFailure(request)); + deployment.activate(); + } + + private boolean shouldIgnoreLockFailure(HttpRequest request) { + return request.getBooleanProperty("force"); + } + + /** + * True if this request should ignore activation failure because the session was made from an active session that is not active now + * @param request a {@link com.yahoo.container.jdisc.HttpRequest} + * @return true if ignore failure + */ + private boolean shouldIgnoreSessionStaleFailure(HttpRequest request) { + return request.getBooleanProperty("force"); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java new file mode 100644 index 00000000000..90b5a7fd869 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentListResponse.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * Represents a request for listing files within an application package. + * + * @author lulf + * @since 5.1 + */ +class SessionContentListResponse extends SessionResponse { + private final Slime slime = new Slime(); + + public SessionContentListResponse(String urlBase, List<ApplicationFile> files) { + super(); + Cursor array = slime.setArray(); + for (ApplicationFile file : files) { + array.addString(urlBase + file.getPath() + (file.isDirectory() ? "/" : "")); + } + } + + @Override + public void render(OutputStream outputStream) throws IOException { + new JsonFormat(true).encode(outputStream, slime); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java new file mode 100644 index 00000000000..045251d0623 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentReadResponse.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.container.jdisc.HttpResponse; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import static com.yahoo.jdisc.http.HttpResponse.Status.*; + +/** + * Represents a response for a request to read contents of a file. + * + * @author lulf + * @since 5.1 + */ +public class SessionContentReadResponse extends HttpResponse { + private final ApplicationFile file; + + public SessionContentReadResponse(ApplicationFile file) { + super(OK); + this.file = file; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + try (InputStream inputStream = file.createInputStream()) { + IOUtils.copyLarge(inputStream, outputStream, new byte[1]); + } + } + + @Override + public String getContentType() { + return HttpResponse.DEFAULT_MIME_TYPE; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java new file mode 100644 index 00000000000..caf38517b6a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusListResponse.java @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.*; + +/** + * Status and md5sum for files within an application package. + * + * @author musum + * @since 5.1.15 + */ +class SessionContentStatusListResponse extends SessionResponse { + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger("SessionContentStatusListResponse"); + + private final Slime slime = new Slime(); + + public SessionContentStatusListResponse(String urlBase, List<ApplicationFile> files) { + super(); + Cursor array = slime.setArray(); + for (ApplicationFile f : files) { + Cursor element = array.addObject(); + element.setString("status", f.getMetaData().getStatus()); + element.setString("md5", f.getMetaData().getMd5()); + element.setString("name", urlBase + f.getPath()); + log.log(LogLevel.DEBUG, "Adding file " + urlBase + f.getPath()); + } + } + + @Override + public void render(OutputStream outputStream) throws IOException { + new JsonFormat(true).encode(outputStream, slime); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java new file mode 100644 index 00000000000..15c852b66c3 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionContentStatusResponse.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.config.application.api.ApplicationFile; + +import java.io.*; + +/** + * Represents a response for a request to show the status and md5sum of a file in the application package. + * + * @author musum + * @since 5.1.15 + */ +public class SessionContentStatusResponse extends SessionResponse { + private final ApplicationFile file; + private final String urlBase; + private final ApplicationFile.MetaData metaData; + private final ObjectMapper mapper = new ObjectMapper(); + + public SessionContentStatusResponse(ApplicationFile file, String urlBase) { + super(); + this.file = file; + this.urlBase = urlBase; + + ApplicationFile.MetaData metaData; + if (file == null) { + metaData = new ApplicationFile.MetaData(ApplicationFile.ContentStatusDeleted, ""); + } else { + metaData = file.getMetaData(); + } + if (metaData == null) { + throw new IllegalArgumentException("Could not find status for '" + file.getPath() + "'"); + } + this.metaData = metaData; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + mapper.writeValue(outputStream, new ResponseData(metaData.status, metaData.md5, urlBase + file.getPath())); + } + + private static class ResponseData { + public final String status; + public final String md5; + public final String name; + + private ResponseData(String status, String md5, String name) { + this.status = status; + this.md5 = md5; + this.name = name; + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java new file mode 100644 index 00000000000..1526a5b4e0e --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreate.java @@ -0,0 +1,115 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.google.common.io.Files; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.io.IOUtils; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.CompressedApplicationInputStream; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.SessionFactory; + +import java.io.File; +import java.io.IOException; + +/** + * Creates a session from an application package, + * or creates a new session from a previous session (with id or the "active" session). + * Handles /application/v2 requests + * + * @author lulf + * @author musum + * @since 5.1.27 + */ +// TODO Rename class +public class SessionCreate { + public final static String APPLICATION_X_GZIP = "application/x-gzip"; + public final static String APPLICATION_ZIP = "application/zip"; + public final static String contentTypeHeader = "Content-Type"; + + private final SessionFactory sessionFactory; + private final LocalSessionRepo localSessionRepo; + private final SessionCreateResponse responseCreator; + + public SessionCreate(SessionFactory sessionFactory, LocalSessionRepo localSessionRepo, SessionCreateResponse responseCreator) { + this.sessionFactory = sessionFactory; + this.localSessionRepo = localSessionRepo; + this.responseCreator = responseCreator; + } + + public HttpResponse createFromExisting(HttpRequest request, Slime deployLog, LocalSession fromSession, TenantName tenant, TimeoutBudget timeoutBudget) { + DeployLogger logger = SessionHandler.createLogger(deployLog, request, + new ApplicationId.Builder().tenant(tenant).applicationName("-").build()); + LocalSession session = sessionFactory.createSessionFromExisting(fromSession, logger, timeoutBudget); + localSessionRepo.addSession(session); + return createResponse(request, session); + } + + public HttpResponse create(HttpRequest request, Slime deployLog, TenantName tenant, TimeoutBudget timeoutBudget) { + validateDataAndHeader(request); + return createSession(request, deployLog, sessionFactory, localSessionRepo, tenant, timeoutBudget); + } + + private HttpResponse createSession(HttpRequest request, Slime deployLog, SessionFactory sessionFactory, LocalSessionRepo localSessionRepo, TenantName tenant, TimeoutBudget timeoutBudget) { + File tempDir = Files.createTempDir(); + File applicationDirectory = decompressApplication(request, tempDir); + DeployLogger logger = SessionHandler.createLogger(deployLog, request, + new ApplicationId.Builder().tenant(tenant).applicationName("-").build()); + String name = getNameProperty(request, logger); + LocalSession session = sessionFactory.createSession(applicationDirectory, name, logger, timeoutBudget); + localSessionRepo.addSession(session); + HttpResponse response = createResponse(request, session); + cleanupApplicationDirectory(tempDir, logger); + return response; + } + + private String getNameProperty(HttpRequest request, DeployLogger logger) { + String name = request.getProperty("name"); + // TODO: Do we need validation of this parameter? + if (name == null) { + name = "default"; + logger.log(LogLevel.INFO, "No application name given, using '" + name + "'"); + } + return name; + } + + private File decompressApplication(HttpRequest request, File tempDir) { + try (CompressedApplicationInputStream application = CompressedApplicationInputStream.createFromCompressedStream(request.getData(), request.getHeader(contentTypeHeader))) { + return application.decompress(tempDir); + } catch (IOException e) { + throw new InternalServerException("Unable to decompress data in body", e); + } + } + + private void cleanupApplicationDirectory(File tempDir, DeployLogger logger) { + logger.log(LogLevel.DEBUG, "Deleting tmp dir '" + tempDir + "'"); + if (!IOUtils.recursiveDeleteDir(tempDir)) { + logger.log(LogLevel.WARNING, "Not able to delete tmp dir '" + tempDir + "'"); + } + } + + + private static void validateDataAndHeader(HttpRequest request) { + if (request.getData() == null) { + throw new BadRequestException("Request contains no data"); + } + String header = request.getHeader(contentTypeHeader); + if (header == null) { + throw new BadRequestException("Request contains no " + contentTypeHeader + " header"); + } else if (!(header.equals(APPLICATION_X_GZIP) || header.equals(APPLICATION_ZIP))) { + throw new BadRequestException("Request contains invalid " + contentTypeHeader + " header, only '" + + APPLICATION_X_GZIP + "' and '" + APPLICATION_ZIP + "' are supported"); + } + } + + private HttpResponse createResponse(HttpRequest request, LocalSession session) { + return responseCreator.createResponse(request.getHost(), request.getPort(), session.getSessionId()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java new file mode 100644 index 00000000000..72810ed394c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionCreateResponse.java @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpResponse; + +/** + * Interface for creating responses for SessionCreateHandler. + * + * @author musum + * @since 5.1.27 + */ +public interface SessionCreateResponse { + + public HttpResponse createResponse(String hostName, int port, long sessionId); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java new file mode 100644 index 00000000000..c8829cf9e4d --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionHandler.java @@ -0,0 +1,112 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.DeployHandlerLogger; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.session.Session; +import com.yahoo.vespa.config.server.session.SessionRepo; + +import java.time.Clock; +import java.time.Duration; +import java.util.concurrent.Executor; + + +/** + * Super class for session handlers, that takes care of checking valid + * methods for a request. Session handlers should subclass this method and + * implement the handleMETHOD methods that it supports. + * + * @author musum + * @since 5.1.14 + */ +public class SessionHandler extends HttpHandler { + public SessionHandler(Executor executor, AccessLog accessLog) { + super(executor, accessLog); + } + + /** + * Gets the raw session id from request (v2). Input request must have a valid path. + * + * @param request a request + * @return a session id + */ + public static String getRawSessionIdV2(HttpRequest request) { + final String path = request.getUri().toString(); + BindingMatch<?> bm = Utils.getBindingMatch(request, "http://*/application/v2/tenant/*/session/*/*"); + if (bm.groupCount() < 4) { + // This would mean the subtype of this doesn't have the correct binding + throw new IllegalArgumentException("Can not get session id from request '" + path + "'"); + } + return bm.group(3); + } + + /** + * Gets session id (as a number) from request (v2). Input request must have a valid path. + * + * @param request a request + * @return a session id + */ + static Long getSessionIdV2(HttpRequest request) { + try { + return Long.parseLong(getRawSessionIdV2(request)); + } catch (NumberFormatException e) { + throw createSessionException(request); + } + } + + private static BadRequestException createSessionException(HttpRequest request) { + return new BadRequestException("Session id in request is not a number, request was '" + + request.getUri().toString() + "'"); + } + + public static <SESSIONTYPE extends Session> SESSIONTYPE getSessionFromRequestV2(SessionRepo<SESSIONTYPE> sessionRepo, HttpRequest request) { + long sessionId = getSessionIdV2(request); + return getSessionFromRequest(sessionRepo, sessionId); + } + + public static <SESSIONTYPE extends Session> SESSIONTYPE getSessionFromRequest(SessionRepo<SESSIONTYPE> sessionRepo, long sessionId) { + SESSIONTYPE session = sessionRepo.getSession(sessionId); + if (session == null) { + throw new NotFoundException("Session " + sessionId + " was not found"); + } + return session; + } + + protected static final Duration DEFAULT_ACTIVATE_TIMEOUT = Duration.ofMinutes(2); + + public static TimeoutBudget getTimeoutBudget(HttpRequest request, Duration defaultTimeout) { + return new TimeoutBudget(Clock.systemUTC(), getRequestTimeout(request, defaultTimeout)); + } + + + protected static Duration getRequestTimeout(HttpRequest request, Duration defaultTimeout) { + if (!request.hasProperty("timeout")) { + return defaultTimeout; + } + try { + return Duration.ofSeconds((long) Double.parseDouble(request.getProperty("timeout"))); + } catch (Exception e) { + return defaultTimeout; + } + } + + static DeployHandlerLogger createLogger(Slime deployLog, HttpRequest request, ApplicationId app) { + return createLogger(deployLog, request.getBooleanProperty("verbose"), app); + } + + public static DeployHandlerLogger createLogger(Slime deployLog, boolean verbose, ApplicationId app) { + return new DeployHandlerLogger(deployLog.get().setArray("log"), verbose, app); + } + + protected Slime createDeployLog() { + Slime deployLog = new Slime(); + deployLog.setObject(); + return deployLog; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java new file mode 100644 index 00000000000..5054e1129d1 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/SessionResponse.java @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; + +import java.io.IOException; +import java.io.OutputStream; + +import static com.yahoo.jdisc.http.HttpResponse.Status.OK; + +/** + * Superclass for responses from session HTTP handlers. Implements the + * render method. + * + * @author musum + * @since 5.1.14 + */ +public class SessionResponse extends HttpResponse { + private final Slime slime; + protected final Cursor root; + + public SessionResponse() { + super(OK); + slime = new Slime(); + root = slime.setObject(); + } + + public SessionResponse(Slime slime, Cursor root) { + super(OK); + this.slime = slime; + this.root = root; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + new JsonFormat(true).encode(outputStream, slime); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java new file mode 100644 index 00000000000..bfdbdd1d4b1 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/UnknownVespaVersionException.java @@ -0,0 +1,17 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +/** + * @author musum + * @since 5.39 + */ +public class UnknownVespaVersionException extends RuntimeException { + + public UnknownVespaVersionException(String message) { + super(message); + } + + public UnknownVespaVersionException(String message, Throwable e) { + super(message, e); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java new file mode 100644 index 00000000000..83f4c836d20 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/Utils.java @@ -0,0 +1,72 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; + +import java.net.URI; + +/** + * Utilities for handlers. + * + * @author musum + * @since 5.1.14 + */ +public class Utils { + + /** + * If request is an HTTP request and a jdisc request, return the {@link com.yahoo.jdisc.application.BindingMatch} + * of the request. Otherwise return a dummy match useful only for testing based on the <code>uriPattern</code> + * supplied. + * + * @param req an {@link com.yahoo.container.jdisc.HttpRequest} + * @param uriPattern a pattern to create a BindingMatch for in tests + * @return match + */ + public static BindingMatch<?> getBindingMatch(HttpRequest req, String uriPattern) { + com.yahoo.jdisc.http.HttpRequest jDiscRequest = req.getJDiscRequest(); + BindingMatch<?> bm = jDiscRequest.getBindingMatch(); + if (bm == null) { + bm = new BindingMatch<>( + new UriPattern(uriPattern).match(URI.create(jDiscRequest.getUri().toString())), + new Object()); + } + return bm; + } + + public static String getUrlBase(HttpRequest request, String pathPrefix) { + return request.getUri().getScheme() + "://" + request.getHost() + ":" + request.getPort() + pathPrefix; + } + + public static Tenant checkThatTenantExists(Tenants tenants, TenantName tenant) { + if (!tenants.tenantsCopy().containsKey(tenant)) { + throw new NotFoundException("Tenant '" + tenant + "' was not found."); + } + return tenants.tenantsCopy().get(tenant); + } + + public static void checkThatTenantDoesNotExist(Tenants tenants, TenantName tenant) { + if (tenants.tenantsCopy().containsKey(tenant)) { + throw new BadRequestException("There already exists a tenant '" + tenant + "'"); + } + } + + public static TenantName getTenantFromRequest(HttpRequest request) { + BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*"); + return TenantName.from(bm.group(2)); + } + + public static TenantName getTenantFromSessionRequest(HttpRequest request) { + BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*/session*"); + return TenantName.from(bm.group(2)); + } + + public static TenantName getTenantFromApplicationsRequest(HttpRequest request) { + BindingMatch<?> bm = getBindingMatch(request, "http://*/application/v2/tenant/*/application*"); + return TenantName.from(bm.group(2)); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java new file mode 100644 index 00000000000..ba7eff7c461 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationContentRequest.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.ContentRequest; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.session.LocalSession; + +/** + * Represents a content request for an application. + * + * @author lulf + * @since 5.3 + */ +class ApplicationContentRequest extends ContentRequest { + + private static final String uriPattern = "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*"; + private final ApplicationId applicationId; + private final Zone zone; + + private ApplicationContentRequest(HttpRequest request, LocalSession session, ApplicationId applicationId, Zone zone) { + super(request, session); + this.applicationId = applicationId; + this.zone = zone; + } + + static ContentRequest create(HttpRequest request, LocalSession session, ApplicationId applicationId, Zone zone) { + return new ApplicationContentRequest(request, session, applicationId, zone); + } + + @Override + protected String getContentPath(HttpRequest request) { + BindingMatch<?> bm = Utils.getBindingMatch(request, uriPattern); + return bm.group(7); + } + + @Override + public String getPathPrefix() { + StringBuilder sb = new StringBuilder(); + sb.append("/application/v2/tenant/").append(applicationId.tenant().value()); + sb.append("/application/").append(applicationId.application().value()); + sb.append("/environment/").append(zone.environment().value()); + sb.append("/region/").append(zone.region().value()); + sb.append("/instance/").append(applicationId.instance().value()); + return sb.toString(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java new file mode 100644 index 00000000000..458a3899d4c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ApplicationHandler.java @@ -0,0 +1,256 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.*; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.RotationsCache; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.application.ApplicationConvergenceChecker; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.application.LogServerLogGrabber; +import com.yahoo.vespa.config.server.http.*; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; +import com.yahoo.vespa.config.server.session.RemoteSession; +import com.yahoo.vespa.config.server.session.RemoteSessionRepo; + +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executor; + +/** + * Handler for deleting a currently active application for a tenant. + * + * @author musum + * @since 5.4 + */ +public class ApplicationHandler extends HttpHandler { + + private static final String REQUEST_PROPERTY_TIMEOUT = "timeout"; + private final Tenants tenants; + private final ContentHandler contentHandler = new ContentHandler(); + private final Optional<Provisioner> hostProvisioner; + private final ApplicationConvergenceChecker convergeChecker; + private final Zone zone; + private final LogServerLogGrabber logServerLogGrabber; + + public ApplicationHandler(Executor executor, AccessLog accessLog, Tenants tenants, + HostProvisionerProvider hostProvisionerProvider, Zone zone, + ApplicationConvergenceChecker convergeChecker, + LogServerLogGrabber logServerLogGrabber) { + super(executor, accessLog); + this.tenants = tenants; + this.hostProvisioner = hostProvisionerProvider.getHostProvisioner(); + this.zone = zone; + this.convergeChecker = convergeChecker; + this.logServerLogGrabber = logServerLogGrabber; + } + + @Override + public HttpResponse handleDELETE(HttpRequest request) { + ApplicationId applicationId = getApplicationIdFromRequest(request); + Tenant tenant = verifyTenantAndApplication(applicationId); + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + final long sessionId = applicationRepo.getSessionIdForApplication(applicationId); + final LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo(); + final LocalSession session = localSessionRepo.getSession(sessionId); + if (session == null) { + return HttpErrorResponse.notFoundError("Unable to delete " + applicationId + " (session id " + sessionId + "):" + + "No local deployment for this application found on this config server"); + } + log.log(LogLevel.INFO, "Deleting " + applicationId); + localSessionRepo.removeSession(session.getSessionId()); + session.delete(); + RotationsCache rotationsCache = new RotationsCache(tenant.getCurator(), tenant.getPath()); + rotationsCache.deleteRotationFromZooKeeper(applicationId); + applicationRepo.deleteApplication(applicationId); + if (hostProvisioner.isPresent()) { + hostProvisioner.get().removed(applicationId); + } + return new DeleteApplicationResponse(Response.Status.OK, applicationId); + } + + + @Override + public HttpResponse handleGET(HttpRequest request) { + ApplicationId applicationId = getApplicationIdFromRequest(request); + Tenant tenant = verifyTenantAndApplication(applicationId); + + if (isServiceConvergeRequest(request)) { + Application application = getApplication(tenant, applicationId); + return convergeChecker.nodeConvergenceCheck(application, getHostFromRequest(request), request.getUri()); + } + if (isContentRequest(request)) { + LocalSession session = SessionHandler.getSessionFromRequest(tenant.getLocalSessionRepo(), tenant.getApplicationRepo().getSessionIdForApplication(applicationId)); + return contentHandler.get(ApplicationContentRequest.create(request, session, applicationId, zone)); + } + Application application = getApplication(tenant, applicationId); + + // TODO: Remove this once the config convegence logic is moved to client and is live for all clusters. + if (isConvergeRequest(request)) { + try { + convergeChecker.waitForConfigConverged(application, new TimeoutBudget(Clock.systemUTC(), durationFromRequestTimeout(request))); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + if (isServiceConvergeListRequest(request)) { + return convergeChecker.listConfigConvergence(application, request.getUri()); + } + return new GetApplicationResponse(Response.Status.OK, application.getApplicationGeneration()); + } + + @Override + public HttpResponse handlePOST(HttpRequest request) { + ApplicationId applicationId = getApplicationIdFromRequest(request); + Tenant tenant = verifyTenantAndApplication(applicationId); + if (request.getUri().getPath().endsWith("restart")) + return handlePostRestart(request, applicationId); + if (request.getUri().getPath().endsWith("log")) + return handlePostLog(request, applicationId, tenant); + throw new NotFoundException("Illegal POST request '" + request.getUri() + "': Must end by /restart or /log"); + } + + private HttpResponse handlePostRestart(HttpRequest request, ApplicationId applicationId) { + if (getBindingMatch(request).groupCount() != 7) + throw new NotFoundException("Illegal POST restart request '" + request.getUri() + + "': Must have 6 arguments but had " + ( getBindingMatch(request).groupCount()-1 ) ); + if (hostProvisioner.isPresent()) + hostProvisioner.get().restart(applicationId, hostFilterFrom(request)); + return new JSONResponse(Response.Status.OK); // return empty + } + + private HttpResponse handlePostLog(HttpRequest request, ApplicationId applicationId, Tenant tenant) { + if (getBindingMatch(request).groupCount() != 7) + throw new NotFoundException("Illegal POST log request '" + request.getUri() + + "': Must have 6 arguments but had " + ( getBindingMatch(request).groupCount()-1 ) ); + Application application = getApplication(tenant, applicationId); + return logServerLogGrabber.grabLog(application); + } + + private HostFilter hostFilterFrom(HttpRequest request) { + return HostFilter.from(request.getProperty("hostname"), + request.getProperty("flavor"), + request.getProperty("clusterType"), + request.getProperty("clusterId")); + } + + private Tenant verifyTenantAndApplication(ApplicationId applicationId) { + Tenant tenant = Utils.checkThatTenantExists(tenants, applicationId.tenant()); + List<ApplicationId> applicationIds = listApplicationIds(tenant); + if ( ! applicationIds.contains(applicationId)) { + throw new NotFoundException("No such application id: " + applicationId); + } + return tenant; + } + + private Duration durationFromRequestTimeout(HttpRequest request) { + long timeoutInSeconds = 60; + if (request.hasProperty(REQUEST_PROPERTY_TIMEOUT)) { + timeoutInSeconds = Long.parseLong(request.getProperty(REQUEST_PROPERTY_TIMEOUT)); + } + return Duration.ofSeconds(timeoutInSeconds); + } + + private Application getApplication(Tenant tenant, ApplicationId applicationId) { + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + RemoteSessionRepo remoteSessionRepo = tenant.getRemoteSessionRepo(); + long sessionId = applicationRepo.getSessionIdForApplication(applicationId); + RemoteSession session = remoteSessionRepo.getSession(sessionId, 0); + return session.ensureApplicationLoaded().getForVersionOrLatest(Optional.empty()); + } + + private List<ApplicationId> listApplicationIds(Tenant tenant) { + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + return applicationRepo.listApplications(); + } + + // Note: Update src/main/resources/configserver-app/services.xml if you do any changes to the bindings + private static BindingMatch<?> getBindingMatch(HttpRequest request) { + return HttpConfigRequests.getBindingMatch(request, + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/content/*", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/log", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/restart", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/converge", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*/serviceconverge/*", + "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*", + "http://*/application/v2/tenant/*/application/*"); + } + + private static boolean isConvergeRequest(HttpRequest request) { + return getBindingMatch(request).groupCount() == 7 && + request.getUri().getPath().endsWith("converge"); + } + + private static boolean isServiceConvergeListRequest(HttpRequest request) { + return getBindingMatch(request).groupCount() == 7 && + request.getUri().getPath().endsWith("serviceconverge"); + } + + private static boolean isServiceConvergeRequest(HttpRequest request) { + return getBindingMatch(request).groupCount() == 8 && + request.getUri().getPath().contains("/serviceconverge/"); + } + + + private static boolean isContentRequest(HttpRequest request) { + return getBindingMatch(request).groupCount() > 7; + } + + private static String getHostFromRequest(HttpRequest req) { + BindingMatch<?> bm = getBindingMatch(req); + return bm.group(7); + } + + private static ApplicationId getApplicationIdFromRequest(HttpRequest req) { + // Two bindings for this: with full app id or only application name + BindingMatch<?> bm = getBindingMatch(req); + if (bm.groupCount() > 4) return createFromRequestFullAppId(bm); + return createFromRequestSimpleAppId(bm); + } + + // The URL pattern with only tenant and application given + private static ApplicationId createFromRequestSimpleAppId(BindingMatch<?> bm) { + TenantName tenant = TenantName.from(bm.group(2)); + ApplicationName application = ApplicationName.from(bm.group(3)); + return new ApplicationId.Builder().tenant(tenant).applicationName(application).build(); + } + + // The URL pattern with full app id given + private static ApplicationId createFromRequestFullAppId(BindingMatch<?> bm) { + String tenant = bm.group(2); + String application = bm.group(3); + String instance = bm.group(6); + return new ApplicationId.Builder() + .tenant(tenant) + .applicationName(application).instanceName(instance) + .build(); + } + + private static class DeleteApplicationResponse extends JSONResponse { + public DeleteApplicationResponse(int status, ApplicationId applicationId) { + super(status); + object.setString("message", "Application '" + applicationId + "' deleted"); + } + } + + private static class GetApplicationResponse extends JSONResponse { + public GetApplicationResponse(int status, long generation) { + super(status); + object.setLong("generation", generation); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java new file mode 100644 index 00000000000..086954c384f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HostHandler.java @@ -0,0 +1,78 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.HostRegistries; +import com.yahoo.vespa.config.server.HostRegistry; +import com.yahoo.vespa.config.server.http.*; + +import java.util.concurrent.Executor; + + +/** + * Handler for getting tenant and application for a given hostname. + * + * @author musum + * @since 5.19 + */ +public class HostHandler extends HttpHandler { + final HostRegistries hostRegistries; + private final Zone zone; + + public HostHandler(Executor executor, AccessLog accessLog, GlobalComponentRegistry globalComponentRegistry) { + super(executor, accessLog); + this.hostRegistries = globalComponentRegistry.getHostRegistries(); + this.zone = globalComponentRegistry.getZone(); + } + + @Override + public HttpResponse handleGET(HttpRequest request) { + String hostname = getBindingMatch(request).group(2); + log.log(LogLevel.DEBUG, "hostname=" + hostname); + + HostRegistry<TenantName> tenantHostRegistry = hostRegistries.getTenantHostRegistry(); + log.log(LogLevel.DEBUG, "hosts in tenant host registry '" + tenantHostRegistry + "' " + tenantHostRegistry.getAllHosts()); + TenantName tenant = tenantHostRegistry.getKeyForHost(hostname); + if (tenant == null) return createError(hostname); + log.log(LogLevel.DEBUG, "tenant=" + tenant); + HostRegistry<ApplicationId> applicationIdHostRegistry = hostRegistries.getApplicationHostRegistry(tenant); + ApplicationId applicationId; + if (applicationIdHostRegistry == null) return createError(hostname); + applicationId = applicationIdHostRegistry.getKeyForHost(hostname); + log.log(LogLevel.DEBUG, "applicationId=" + applicationId); + if (applicationId == null) { + return createError(hostname); + } else { + log.log(LogLevel.DEBUG, "hosts in application host registry '" + applicationIdHostRegistry + "' " + applicationIdHostRegistry.getAllHosts()); + return new HostResponse(Response.Status.OK, applicationId, zone); + } + } + + private HttpErrorResponse createError(String hostname) { + return HttpErrorResponse.notFoundError("Could not find any application using host '" + hostname + "'"); + } + + private static BindingMatch<?> getBindingMatch(HttpRequest request) { + return HttpConfigRequests.getBindingMatch(request, "http://*/application/v2/host/*"); + } + + private static class HostResponse extends JSONResponse { + public HostResponse(int status, ApplicationId applicationId, Zone zone) { + super(status); + object.setString("tenant", applicationId.tenant().value()); + object.setString("application", applicationId.application().value()); + object.setString("environment", zone.environment().value()); + object.setString("region", zone.region().value()); + object.setString("instance", applicationId.instance().value()); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java new file mode 100644 index 00000000000..b63c52a26bb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpConfigRequests.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.net.URI; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.jdisc.application.UriPattern.Match; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.http.NotFoundException; + +/** + * Helpers for v2 config REST API + * + * @author vegardh + */ +public class HttpConfigRequests { + + static final String RECURSIVE_QUERY_PROPERTY = "recursive"; + + /** + * Produces the binding match for the request. If it's not available on the jDisc request, create one for + * testing using the long and short app id URL patterns given. + * @param req an {@link com.yahoo.container.jdisc.HttpRequest} + * @param patterns A list of patterns that should be matched if no match on binding. + * @return match + */ + public static BindingMatch<?> getBindingMatch(HttpRequest req, String ... patterns) { + com.yahoo.jdisc.http.HttpRequest jDiscRequest = req.getJDiscRequest(); + if (jDiscRequest==null) throw new IllegalArgumentException("No JDisc request for: " + req.getUri()); + BindingMatch<?> jdBm = jDiscRequest.getBindingMatch(); + if (jdBm!=null) return jdBm; + + // If not, use provided patterns + for (String pattern : patterns) { + UriPattern fullAppIdPattern = new UriPattern(pattern); + URI uri = req.getUri(); + Match match = fullAppIdPattern.match(uri); + if (match!=null) return new BindingMatch<>(match, new Object()); + } + throw new IllegalArgumentException("Illegal url for config request: " + req.getUri()); + } + + + static RequestHandler getRequestHandler(Tenants tenants, TenantRequest request) { + Tenant tenant = tenants.tenantsCopy().get(request.getApplicationId().tenant()); + if (tenant==null) throw new NotFoundException("No such tenant: "+request.getApplicationId().tenant()); + return tenant.getRequestHandler(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java new file mode 100644 index 00000000000..6c872578324 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpGetConfigHandler.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.inject.Inject; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.protocol.ConfigResponse; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.http.HttpConfigRequest; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.HttpHandler; + +import java.util.Optional; +import java.util.concurrent.Executor; + +/** + * HTTP handler for a getConfig operation + * + * @author lulf + * @since 5.1 + */ +public class HttpGetConfigHandler extends HttpHandler { + + private final Tenants tenants; + + @Inject + public HttpGetConfigHandler(Executor executor, AccessLog accesslog, Tenants tenants) { + super(executor, accesslog); + this.tenants = tenants; + } + + @Override + public HttpResponse handleGET(HttpRequest req) { + HttpConfigRequest request = HttpConfigRequest.createFromRequestV2(req); + RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, request); + HttpConfigRequest.validateRequestKey(request.getConfigKey(), requestHandler, request.getApplicationId()); + return HttpConfigResponse.createFromConfig(resolveConfig(request, requestHandler)); + } + + private ConfigResponse resolveConfig(HttpConfigRequest request, RequestHandler requestHandler) { + log.log(LogLevel.DEBUG, "nocache=" + request.noCache()); + ConfigResponse config = requestHandler.resolveConfig(request.getApplicationId(), request, Optional.empty()); + if (config == null) HttpConfigRequest.throwModelNotReady(); + return config; + } +}
\ No newline at end of file diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java new file mode 100644 index 00000000000..0358a9a7046 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsHandler.java @@ -0,0 +1,180 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.*; +import java.util.concurrent.Executor; +import com.google.inject.Inject; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.path.Path; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.HttpHandler; +import com.yahoo.vespa.config.server.http.Utils; +import static com.yahoo.jdisc.http.HttpResponse.Status.*; + +/** + * Handler for a list configs operation. Lists all configs in model for a given application and tenant. + * + * @author vegardh + * @since 5.3 + */ +public class HttpListConfigsHandler extends HttpHandler { + private final Tenants tenants; + private final Zone zone; + + @Inject + public HttpListConfigsHandler(Executor executor, AccessLog accesslog, Tenants tenants, Zone zone) { + super(executor, accesslog); + this.tenants = tenants; + this.zone = zone; + } + + @Override + public HttpResponse handleGET(HttpRequest req) { + HttpListConfigsRequest listReq = HttpListConfigsRequest.createFromListRequest(req); + RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, listReq); + ApplicationId appId = listReq.getApplicationId(); + Set<ConfigKey<?>> configs = requestHandler.listConfigs(appId, Optional.empty(), listReq.isRecursive()); + String urlBase = getUrlBase(req, listReq, appId, zone); + Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(appId, Optional.empty()); + return new ListConfigsResponse(configs, allConfigs, urlBase, listReq.isRecursive()); + } + + static String getUrlBase(HttpRequest req, HttpListConfigsRequest listReq, ApplicationId appId, Zone zone) { + if (listReq.isFullAppId()) + return Utils.getUrlBase(req, + Path.fromString("/config/v2/tenant/"). + append(appId.tenant().value()). + append("application"). + append(appId.application().value()). + append("environment"). + append(zone.environment().value()). + append("region"). + append(zone.region().value()). + append("instance"). + append(appId.instance().value()).getAbsolute()+"/"); + else + return Utils.getUrlBase(req, + Path.fromString("/config/v2/tenant/"). + append(appId.tenant().value()). + append("application"). + append(appId.application().value()).getAbsolute()+"/"); + } + + static class ListConfigsResponse extends HttpResponse { + private final List<ConfigKey<?>> configs; + private final Set<ConfigKey<?>> allConfigs; + private final String urlBase; + private final boolean recursive; + + /** + * New list response + * + * @param configs the configs to include in the list + * @param urlBase for example "http://foo.com:19071/config/v1/ (configs are appended to the listed URLs based on configs list) + * @param recursive list recursively + */ + public ListConfigsResponse(Set<ConfigKey<?>> configs, Set<ConfigKey<?>> allConfigs, String urlBase, boolean recursive) { + super(OK); + this.configs = new ArrayList<>(configs); + Collections.sort(this.configs); + this.allConfigs = allConfigs; + this.urlBase = urlBase; + this.recursive = recursive; + } + + /** + * The listing URL for this config in this service + * + * @param key config key + * @param rec recursive + * @return url + */ + public String toUrl(ConfigKey<?> key, boolean rec) { + return urlBase + key.getNamespace() + "." + key.getName() + configIdUrlPart(rec, key.getConfigId()); + } + + // Do not end with / if it's a recursive listing. Furthermore, don't do it if it's the empty config id (special handling of the root config id). + private String configIdUrlPart(boolean rec, String configId) { + if ("".equals(configId)) return ""; + if (rec) return "/" + configId; + return "/" + configId + "/"; + } + + @Override + public void render(OutputStream outputStream) throws IOException { + Slime slime = new Slime(); + Cursor root = slime.setObject(); + Cursor array; + if (!recursive) { + array = root.setArray("children"); + for (ConfigKey<?> key : keysThatHaveAChildWithSameName(configs, allConfigs)) { + array.addString(toUrl(key, false)); + } + } + array = root.setArray("configs"); + + Collection<ConfigKey<?>> cfs; + if (recursive) { + cfs = configs; + } else { + cfs = withParentConfigId(configs); + } + for (ConfigKey<?> key : cfs) { + array.addString(toUrl(key, true)); + } + new JsonFormat(true).encode(outputStream, slime); + } + + public static Set<ConfigKey<?>> keysThatHaveAChildWithSameName(Collection<ConfigKey<?>> keys, Set<ConfigKey<?>> allConfigs) { + Set<ConfigKey<?>> ret = new LinkedHashSet<>(); + for (ConfigKey<?> k : keys) { + if (ListConfigsResponse.hasAChild(k, allConfigs)) ret.add(k); + } + return ret; + } + + // Do we do this already somewhere? + private static Set<ConfigKey<?>> withParentConfigId(Collection<ConfigKey<?>> keys) { + Set<ConfigKey<?>> ret = new LinkedHashSet<>(); + for (ConfigKey<?> k : keys) { + ret.add(new ConfigKey<>(k.getName(), parentConfigId(k.getConfigId()), k.getNamespace())); + } + return ret; + } + + static String parentConfigId(String id) { + if (id==null) return null; + if (!id.contains("/")) return ""; + return id.substring(0, id.lastIndexOf('/')); + } + + static boolean hasAChild(ConfigKey<?> key, Set<ConfigKey<?>> keys) { + if ("".equals(key.getConfigId())) return false; + for (ConfigKey<?> k : keys) { + if (!k.getName().equals(key.getName())) continue; + if ("".equals(k.getConfigId())) continue; + if (k.getConfigId().equals(key.getConfigId())) continue; + if (k.getConfigId().startsWith(key.getConfigId())) return true; + } + return false; + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java new file mode 100644 index 00000000000..1140d6e769f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListConfigsRequest.java @@ -0,0 +1,156 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.collections.Tuple2; +import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.HttpConfigRequest; + +/** + * A request to list config, bound to tenant and app id. Optionally bound to a config key, for request for named config. + * + * @author vegardh + * + */ +public class HttpListConfigsRequest implements TenantRequest { + private final ConfigKey<?> key; // non-null if it's a named list request + private final ApplicationId appId; + private final boolean recursive; + private final boolean fullAppId; + + private HttpListConfigsRequest(ConfigKey<?> key, ApplicationId appId, boolean recursive, boolean fullAppId) { + this.key = key; + this.appId = appId; + this.recursive = recursive; + this.fullAppId = fullAppId; + } + + public static HttpListConfigsRequest createFromListRequest(HttpRequest req) { + BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req, + "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/", + "http://*/config/v2/tenant/*/application/*/"); + if (bm.groupCount()>4) return createFromListRequestFullAppId(req, bm); + return createFromListRequestSimpleAppId(req, bm); + } + + private static HttpListConfigsRequest createFromListRequestSimpleAppId(HttpRequest req, BindingMatch<?> bm) { + TenantName tenant = TenantName.from(bm.group(2)); + ApplicationName application = ApplicationName.from(bm.group(3)); + return new HttpListConfigsRequest(null, new ApplicationId.Builder().tenant(tenant).applicationName(application).build(), + req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), false); + } + + private static HttpListConfigsRequest createFromListRequestFullAppId(HttpRequest req, BindingMatch<?> bm) { + String tenant = bm.group(2); + String application = bm.group(3); + String environment = bm.group(4); + String region = bm.group(5); + String instance = bm.group(6); + + ApplicationId appId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName(application) + .instanceName(instance) + .build(); + return new HttpListConfigsRequest(null, appId, + req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), true); + } + + public static HttpListConfigsRequest createFromNamedListRequest(HttpRequest req) { + // http://*/config/v2/tenant/*/application/*/*/ + // http://*/config/v2/tenant/*/application/*/*/*/ + // http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/ + // http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/ + BindingMatch<?> bm = HttpConfigRequests.getBindingMatch(req, + "http://*/config/v2/tenant/*/application/*/environment/*/region/*/instance/*/*/*/", + "http://*/config/v2/tenant/*/application/*/*/*/"); + if (bm.groupCount()>6) return createFromNamedListRequestFullAppId(req, bm); + return createFromNamedListRequestSimpleAppId(req, bm); + } + + private static HttpListConfigsRequest createFromNamedListRequestSimpleAppId(HttpRequest req, BindingMatch<?> bm) { + TenantName tenant = TenantName.from(bm.group(2)); + ApplicationName application = ApplicationName.from(bm.group(3)); + String conf = bm.group(4); + String cId; + String cName; + String cNamespace; + if (bm.groupCount() >= 6) { + cId = bm.group(5); + } else { + cId = ""; + } + Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(conf); + cName = nns.first; + cNamespace = nns.second; + ConfigKey<?> key = new ConfigKey<>(cName, cId, cNamespace); + return new HttpListConfigsRequest(key, new ApplicationId.Builder().tenant(tenant).applicationName(application).build(), + req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), false); + } + + private static HttpListConfigsRequest createFromNamedListRequestFullAppId(HttpRequest req, BindingMatch<?> bm) { + String tenant = bm.group(2); + String application = bm.group(3); + String environment = bm.group(4); + String region = bm.group(5); + String instance = bm.group(6); + String conf = bm.group(7); + String cId; + String cName; + String cNamespace; + if (bm.groupCount() >= 9) { + cId = bm.group(8); + } else { + cId = ""; + } + Tuple2<String, String> nns = HttpConfigRequest.nameAndNamespace(conf); + cName = nns.first; + cNamespace = nns.second; + ConfigKey<?> key = new ConfigKey<>(cName, cId, cNamespace); + ApplicationId appId = new ApplicationId.Builder() + .tenant(tenant) + .applicationName(application) + .instanceName(instance) + .build(); + return new HttpListConfigsRequest(key, appId, + req.getBooleanProperty(HttpConfigRequests.RECURSIVE_QUERY_PROPERTY), true); + } + + /** + * The application id of the request + * @return app id + */ + @Override + public ApplicationId getApplicationId() { + return appId; + } + + /** + * True if the request was of the recursive form + * @return recursive + */ + public boolean isRecursive() { + return recursive; + } + + /** + * True if this was created using a URL with tenant, application, environment, region and instance; false if only tenant and application + * @return true if full app id used + */ + public boolean isFullAppId() { + return fullAppId; + } + + /** + * Returns the config key of the request if it was for a named config, or null if it was just a listing request + * @return key or null + */ + public ConfigKey<?> getKey() { + return key; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java new file mode 100644 index 00000000000..ed4accb386b --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/HttpListNamedConfigsHandler.java @@ -0,0 +1,49 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Executor; + +import com.google.inject.Inject; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.server.RequestHandler; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.HttpConfigRequest; +import com.yahoo.vespa.config.server.http.HttpHandler; + +/** + * Handler for a list named configs operation. Lists all configs in model for a given application and tenant, config name and optionally config id. + * + * @author vegardh + * @since 5.3 + */ +public class HttpListNamedConfigsHandler extends HttpHandler { + + private final Tenants tenants; + private final Zone zone; + + @Inject + public HttpListNamedConfigsHandler(Executor executor, AccessLog accesslog, Tenants tenants, Zone zone) { + super(executor, accesslog); + this.tenants = tenants; + this.zone = zone; + } + + @Override + public HttpResponse handleGET(HttpRequest req) { + HttpListConfigsRequest listReq = HttpListConfigsRequest.createFromNamedListRequest(req); + RequestHandler requestHandler = HttpConfigRequests.getRequestHandler(tenants, listReq); + ApplicationId appId = listReq.getApplicationId(); + HttpConfigRequest.validateRequestKey(listReq.getKey(), requestHandler, appId); + Set<ConfigKey<?>> configs = requestHandler.listNamedConfigs(appId, Optional.empty(), listReq.getKey(), listReq.isRecursive()); + String urlBase = HttpListConfigsHandler.getUrlBase(req, listReq, appId, zone); + Set<ConfigKey<?>> allConfigs = requestHandler.allConfigsProduced(appId, Optional.empty()); + return new HttpListConfigsHandler.ListConfigsResponse(configs, allConfigs, urlBase, listReq.isRecursive()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java new file mode 100644 index 00000000000..e77c3928f11 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsHandler.java @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.common.base.Function; +import com.google.common.collect.Collections2; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.Response; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.http.HttpHandler; +import com.yahoo.vespa.config.server.http.Utils; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Handler for listing currently active applications for a tenant. + * + * @author lulf + * @since 5.1 + */ +public class ListApplicationsHandler extends HttpHandler { + private final Tenants tenants; + private final Zone zone; + public ListApplicationsHandler(Executor executor, AccessLog accessLog, Tenants tenants, Zone zone) { + super(executor, accessLog); + this.tenants = tenants; + this.zone = zone; + } + + @Override + public HttpResponse handleGET(HttpRequest request) { + TenantName tenantName = Utils.getTenantFromApplicationsRequest(request); + final String urlBase = Utils.getUrlBase(request, "/application/v2/tenant/" + tenantName + "/application/"); + + List<ApplicationId> applicationIds = listApplicationIds(tenantName); + Collection<String> applicationUrls = Collections2.transform(applicationIds, new Function<ApplicationId, String>() { + @Override + public String apply(ApplicationId id) { + return createUrlStringFromId(urlBase, id, zone); + } + }); + return new ListApplicationsResponse(Response.Status.OK, applicationUrls); + } + + private List<ApplicationId> listApplicationIds(TenantName tenantName) { + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + return applicationRepo.listApplications(); + } + + private static String createUrlStringFromId(String urlBase, ApplicationId id, Zone zone) { + StringBuilder sb = new StringBuilder(); + sb.append(urlBase).append(id.application().value()); + sb.append("/environment/").append(zone.environment().value()); + sb.append("/region/").append(zone.region().value()); + sb.append("/instance/").append(id.instance().value()); + return sb.toString(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java new file mode 100644 index 00000000000..3efd50fade6 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListApplicationsResponse.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.JsonFormat; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; + +/** + * Response that lists applications. + * + * @author lulf + * @since 5.1 + */ +public class ListApplicationsResponse extends HttpResponse { + private final Slime slime = new Slime(); + public ListApplicationsResponse(int status, Collection<String> applications) { + super(status); + Cursor array = slime.setArray(); + for (String url : applications) { + array.addString(url); + } + } + + @Override + public void render(OutputStream outputStream) throws IOException { + new JsonFormat(true).encode(outputStream, slime); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java new file mode 100644 index 00000000000..0fd456b6bd0 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsHandler.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.util.concurrent.Executor; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.vespa.config.server.http.HttpHandler; +import com.yahoo.vespa.config.server.Tenants; + +/** + * Handler to list tenants in the configserver + * + * @author vegardh + * + */ +public class ListTenantsHandler extends HttpHandler { + + private final Tenants tenants; + + + public ListTenantsHandler(Executor executor, AccessLog accessLog, Tenants tenants) { + super(executor, accessLog); + this.tenants = tenants; + } + + + @Override + protected HttpResponse handleGET(HttpRequest request) { + return new ListTenantsResponse(tenants.tenantsCopy().keySet()); + } + + + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java new file mode 100644 index 00000000000..35ff6faa89f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/ListTenantsResponse.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + + +import com.yahoo.config.provision.TenantName; +import com.yahoo.slime.Cursor; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.SessionResponse; + +import java.util.Collection; + +/** + * Tenant list response + * + * @author vegardh + * + */ +public class ListTenantsResponse extends SessionResponse { + private final Collection<TenantName> tenantNames; + + public ListTenantsResponse(final Collection<TenantName> tenants) { + super(); + this.tenantNames = tenants; + Cursor tenantArray = this.root.setArray("tenants"); + synchronized (tenants) { + for (final TenantName tenantName : tenants) { + tenantArray.addString(tenantName.value()); + } + } + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + + public Collection<TenantName> getTenantNames() { + return tenantNames; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java new file mode 100644 index 00000000000..ee926f39cad --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveHandler.java @@ -0,0 +1,59 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.util.Optional; +import java.util.concurrent.Executor; + +import com.google.inject.Inject; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.http.SessionActiveHandlerBase; +import com.yahoo.vespa.config.server.http.SessionHandler; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.session.LocalSession; + +/** + * Handler that activates a session given by tenant and id (PUT). + * + * @author vegardh + * @since 5.1 + */ +public class SessionActiveHandler extends SessionActiveHandlerBase { + + private final Tenants tenants; + private final Optional<Provisioner> hostProvisioner; + private final Zone zone; + + @Inject + public SessionActiveHandler(Executor executor, + AccessLog accessLog, + Tenants tenants, + HostProvisionerProvider hostProvisionerProvider, + Zone zone) { + super(executor, accessLog); + this.tenants = tenants; + this.hostProvisioner = hostProvisionerProvider.getHostProvisioner(); + this.zone = zone; + } + + @Override + protected HttpResponse handlePUT(HttpRequest request) { + TimeoutBudget timeoutBudget = getTimeoutBudget(request, SessionHandler.DEFAULT_ACTIVATE_TIMEOUT); + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request"); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + LocalSession localSession = getSessionFromRequestV2(tenant.getLocalSessionRepo(), request); + activate(request, tenant.getLocalSessionRepo(), tenant.getActivateLock(), timeoutBudget, hostProvisioner, localSession); + return new SessionActiveResponse(localSession.getMetaData().getSlime(), tenantName, request, localSession, zone); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java new file mode 100644 index 00000000000..b5948c7e786 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionActiveResponse.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.slime.Slime; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.http.SessionResponse; +import com.yahoo.vespa.config.server.session.LocalSession; + +public class SessionActiveResponse extends SessionResponse { + + public SessionActiveResponse(Slime metaData, TenantName tenantName, HttpRequest request, LocalSession session, Zone zone) { + super(metaData, metaData.get()); + String message = "Session " + session.getSessionId() + " for tenant '" + tenantName + "' activated."; + root.setString("tenant", tenantName.value()); + root.setString("message", message); + final ApplicationId applicationId = session.getApplicationId(); + root.setString("url", "http://" + request.getHost() + ":" + request.getPort() + + "/application/v2/tenant/" + tenantName + + "/application/" + applicationId.application().value() + + "/environment/" + zone.environment().value() + + "/region/" + zone.region().value() + + "/instance/" + applicationId.instance().value()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java new file mode 100644 index 00000000000..669ec049770 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentHandler.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.inject.Inject; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.http.ContentHandler; +import com.yahoo.vespa.config.server.http.NotFoundException; +import com.yahoo.vespa.config.server.http.SessionHandler; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; + +import java.util.concurrent.Executor; + +/** + * A handler that will return content or content status for files or directories + * in the session's application package + * + * @author lulf + * @since 5.1 + */ +public class SessionContentHandler extends SessionHandler { + private final Tenants tenants; + private final ContentHandler contentHandler = new ContentHandler(); + + @Inject + public SessionContentHandler(Executor executor, AccessLog accessLog, Tenants tenants) { + super(executor, accessLog); + this.tenants = tenants; + } + + @Override + public HttpResponse handleGET(HttpRequest request) { + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request"); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + LocalSession session = getLocalSession(request, tenant.getLocalSessionRepo()); + return contentHandler.get(SessionContentRequestV2.create(request, session)); + } + + private LocalSession getLocalSession(HttpRequest request, LocalSessionRepo localSessionRepo) { + LocalSession session = getSessionFromRequestV2(localSessionRepo, request); + if (session == null) { + throw new NotFoundException("No valid session id in request " + request.getUri().toString()); + } + return session; + } + + @Override + public HttpResponse handlePUT(HttpRequest request) { + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request"); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + return contentHandler.put(SessionContentRequestV2.create(request, getSessionFromRequestV2(tenant.getLocalSessionRepo(), request))); + } + + @Override + public HttpResponse handleDELETE(HttpRequest request) { + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request"); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + return contentHandler.delete(SessionContentRequestV2.create(request, getSessionFromRequestV2(tenant.getLocalSessionRepo(), request))); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java new file mode 100644 index 00000000000..16d3fd6802a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionContentRequestV2.java @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.jdisc.application.BindingMatch; +import com.yahoo.vespa.config.server.http.ContentRequest; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.session.LocalSession; + +/** + * Requests for content and content status (v2) + * are handled by this class. + * + * @author musum + * @since 5.3 + */ +class SessionContentRequestV2 extends ContentRequest { + private static final String uriPattern = "http://*/application/v2/tenant/*/session/*/content/*"; + private final TenantName tenantName; + private final long sessionId; + + private SessionContentRequestV2(HttpRequest request, LocalSession session, TenantName tenantName) { + super(request, session); + this.tenantName = tenantName; + this.sessionId = session.getSessionId(); + } + + static ContentRequest create(HttpRequest request, LocalSession session) { + return new SessionContentRequestV2(request, session, Utils.getTenantFromSessionRequest(request)); + } + + @Override + public String getPathPrefix() { + return "/application/v2/tenant/" + tenantName.value() + "/session/" + sessionId; + } + + @Override + protected String getContentPath(HttpRequest request) { + BindingMatch<?> bm = Utils.getBindingMatch(request, uriPattern); + return bm.group(4); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java new file mode 100644 index 00000000000..90d8ba63892 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateHandler.java @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.jdisc.application.UriPattern; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.http.BadRequestException; +import com.yahoo.vespa.config.server.http.SessionCreate; +import com.yahoo.vespa.config.server.http.SessionHandler; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.session.LocalSession; +import com.yahoo.vespa.config.server.session.LocalSessionRepo; + +import java.net.URI; +import java.time.Duration; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +/** + * A handler that is able to create a session from an application package, + * or create a new session from a previous session (with id or the "active" session). + * Handles /application/v2/ requests + * + * @author musum + * @since 5.1 + */ +public class SessionCreateHandler extends SessionHandler { + private static final Logger log = Logger.getLogger(SessionCreateHandler.class.getName()); + private final Tenants tenants; + private static final String fromPattern = "http://*/application/v2/tenant/*/application/*/environment/*/region/*/instance/*"; + private final ConfigserverConfig configserverConfig; + + @Inject + public SessionCreateHandler(Executor executor, AccessLog accessLog, Tenants tenants, ConfigserverConfig configserverConfig) { + super(executor, accessLog); + this.tenants = tenants; + this.configserverConfig = configserverConfig; + } + + @Override + protected HttpResponse handlePOST(HttpRequest request) { + Slime deployLog = createDeployLog(); + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request"); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + final SessionCreate sessionCreate = new SessionCreate(tenant.getSessionFactory(), tenant.getLocalSessionRepo(), + new SessionCreateResponseV2(tenant, deployLog, deployLog.get())); + TimeoutBudget timeoutBudget = SessionHandler.getTimeoutBudget(request, Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout())); + if (request.hasProperty("from")) { + LocalSession fromSession = getExistingSession(tenant, request); + return sessionCreate.createFromExisting(request, deployLog, fromSession, tenantName, timeoutBudget); + } else { + return sessionCreate.create(request, deployLog, tenantName, timeoutBudget); + } + } + + private static LocalSession getExistingSession(Tenant tenant, HttpRequest request) { + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + LocalSessionRepo localSessionRepo = tenant.getLocalSessionRepo(); + ApplicationId applicationId = getFromProperty(request); + return SessionHandler.getSessionFromRequest(localSessionRepo, applicationRepo.getSessionIdForApplication(applicationId)); + } + + private static ApplicationId getFromProperty(HttpRequest request) { + String from = request.getProperty("from"); + if (from == null || "".equals(from)) { + throw new BadRequestException("Parameter 'from' has illegal value '" + from + "'"); + } + return getAndValidateFromParameter(URI.create(from)); + } + + private static ApplicationId getAndValidateFromParameter(URI from) { + UriPattern.Match match = new UriPattern(fromPattern).match(from); + if (match == null || match.groupCount() < 7) { + throw new BadRequestException("Parameter 'from' has illegal value '" + from + "'"); + } + return new ApplicationId.Builder() + .tenant(match.group(2)) + .applicationName(match.group(3)) + .instanceName(match.group(6)).build(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java new file mode 100644 index 00000000000..5fab4b97407 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionCreateResponseV2.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.slime.Cursor; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.http.SessionCreateResponse; +import com.yahoo.vespa.config.server.http.SessionResponse; + +/** + * Creates a response for SessionCreateHandler (v2). + * + * @author musum + * @since 5.1.27 + */ +public class SessionCreateResponseV2 extends SessionResponse implements SessionCreateResponse { + private final Tenant tenant; + + public SessionCreateResponseV2(Tenant tenant, Slime deployLog, Cursor root) { + super(deployLog, root); + this.tenant = tenant; + } + + @Override + public HttpResponse createResponse(String hostName, int port, long sessionId) { + String tenantName = tenant.getName().value(); + String path = "http://" + hostName + ":" + port + "/application/v2/tenant/" + tenantName + "/session/" + sessionId; + + this.root.setString("tenant", tenantName); + this.root.setString("session-id", Long.toString(sessionId)); + this.root.setString("prepared", path + "/prepared"); + this.root.setString("content", path + "/content/"); + this.root.setString("message", "Session " + sessionId + " for tenant '" + tenantName + "' created."); + return this; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java new file mode 100644 index 00000000000..9a0cc7e6d16 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareHandler.java @@ -0,0 +1,114 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.log.LogLevel; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.config.server.configchange.RefeedActions; +import com.yahoo.vespa.config.server.configchange.RefeedActionsFormatter; +import com.yahoo.vespa.config.server.configchange.RestartActionsFormatter; +import com.yahoo.vespa.config.server.http.SessionHandler; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.session.*; + +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A handler that prepares a session given by an id in the request. v2 of application API + * + * @author musum + * @since 5.1.29 + */ +public class SessionPrepareHandler extends SessionHandler { + private static final Logger log = Logger.getLogger(SessionPrepareHandler.class.getName()); + + private final Tenants tenants; + private final ConfigserverConfig configserverConfig; + + @Inject + public SessionPrepareHandler(Executor executor, + AccessLog accessLog, + Tenants tenants, + ConfigserverConfig configserverConfig) { + super(executor, accessLog); + this.tenants = tenants; + this.configserverConfig = configserverConfig; + } + + @Override + protected HttpResponse handlePUT(HttpRequest request) { + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + Tenant tenantContext = Utils.checkThatTenantExists(tenants, tenantName); + LocalSession session = getSessionFromRequestV2(tenantContext.getLocalSessionRepo(), request); + if (Session.Status.ACTIVATE.equals(session.getStatus())) { + throw new IllegalArgumentException("Session is active: " + session.getSessionId()); + } + log.log(LogLevel.DEBUG, "session=" + session); + boolean verbose = request.getBooleanProperty("verbose"); + Slime rawDeployLog = createDeployLog(); + PrepareParams prepParams = PrepareParams.fromHttpRequest(request, tenantName, configserverConfig); + // An app id currently using only the name + ApplicationId appId = prepParams.getApplicationId(); + DeployLogger logger = createLogger(rawDeployLog, verbose, appId); + ConfigChangeActions actions = session.prepare(logger, prepParams, getCurrentActiveApplicationSet(tenantContext, appId), tenantContext.getPath()); + logConfigChangeActions(actions, logger); + log.log(LogLevel.INFO, Tenants.logPre(appId)+"Session "+session.getSessionId()+" prepared successfully. "); + return new SessionPrepareResponse(rawDeployLog, tenantContext, request, session, actions); + } + + private static void logConfigChangeActions(ConfigChangeActions actions, DeployLogger logger) { + if ( ! actions.getRestartActions().getEntries().isEmpty()) { + logger.log(Level.WARNING, "Change(s) between active and new application that require restart:\n" + + new RestartActionsFormatter(actions.getRestartActions()).format()); + } + if ( ! actions.getRefeedActions().getEntries().isEmpty()) { + boolean allAllowed = actions.getRefeedActions().getEntries().stream().allMatch(RefeedActions.Entry::allowed); + logger.log(allAllowed ? Level.INFO : Level.WARNING, + "Change(s) between active and new application that may require re-feed:\n" + + new RefeedActionsFormatter(actions.getRefeedActions()).format()); + } + } + + @Override + protected HttpResponse handleGET(HttpRequest request) { + TenantName tenantName = Utils.getTenantFromSessionRequest(request); + log.log(LogLevel.DEBUG, "Found tenant '" + tenantName + "' in request"); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + RemoteSession session = getSessionFromRequestV2(tenant.getRemoteSessionRepo(), request); + if (Session.Status.ACTIVATE.equals(session.getStatus())) + throw new IllegalArgumentException("Session is active: " + session.getSessionId()); + if (!Session.Status.PREPARE.equals(session.getStatus())) + throw new IllegalArgumentException("Session not prepared: " + session.getSessionId()); + return new SessionPrepareResponse(createDeployLog(), tenant, request, session, new ConfigChangeActions()); + } + + private static Optional<ApplicationSet> getCurrentActiveApplicationSet(Tenant tenant, ApplicationId appId) { + Optional<ApplicationSet> currentActiveApplicationSet = Optional.empty(); + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + try { + long currentActiveSessionId = applicationRepo.getSessionIdForApplication(appId); + final RemoteSession currentActiveSession = tenant.getRemoteSessionRepo().getSession(currentActiveSessionId); + if (currentActiveSession != null) { + currentActiveApplicationSet = Optional.ofNullable(currentActiveSession.ensureApplicationLoaded()); + } + } catch (IllegalArgumentException e) { + // Do nothing if we have no currently active session + } + return currentActiveApplicationSet; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java new file mode 100644 index 00000000000..dbc36bbc948 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/SessionPrepareResponse.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.slime.Slime; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActionsSlimeConverter; +import com.yahoo.vespa.config.server.http.SessionResponse; +import com.yahoo.vespa.config.server.session.Session; + +/** + * Creates a response for SessionPrepareHandler. + * + * @author musum + * @since 5.1.28 + */ +class SessionPrepareResponse extends SessionResponse { + + public SessionPrepareResponse(Slime deployLog, Tenant tenant, HttpRequest request, Session session, ConfigChangeActions actions) { + super(deployLog, deployLog.get()); + String message = "Session " + session.getSessionId() + " for tenant '" + tenant.getName() + "' prepared."; + this.root.setString("tenant", tenant.getName().value()); + this.root.setString("activate", "http://" + request.getHost() + ":" + request.getPort() + "/application/v2/tenant/" + tenant.getName() + "/session/" + session.getSessionId() + "/active"); + root.setString("message", message); + new ConfigChangeActionsSlimeConverter(actions).toSlime(root); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java new file mode 100644 index 00000000000..469736005b8 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantCreateResponse.java @@ -0,0 +1,26 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.SessionResponse; + +/** + * Response for tenant create + * + * @author vegardh + * + */ +public class TenantCreateResponse extends SessionResponse { + + public TenantCreateResponse(TenantName tenant) { + super(); + this.root.setString("message", "Tenant "+tenant+" created."); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java new file mode 100644 index 00000000000..dbadc77c9dd --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantDeleteResponse.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.SessionResponse; + +/** + * Response for tenant delete + * + * @author vegardh + * + */ +public class TenantDeleteResponse extends SessionResponse { + + public TenantDeleteResponse(TenantName tenant) { + super(); + this.root.setString("message", "Tenant "+tenant+" deleted."); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java new file mode 100644 index 00000000000..99393cd351a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantGetResponse.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.http.HttpConfigResponse; +import com.yahoo.vespa.config.server.http.SessionResponse; + +/** + * Response for tenant create + * + * @author musum + */ +public class TenantGetResponse extends SessionResponse { + + public TenantGetResponse(TenantName tenant) { + super(); + this.root.setString("message", "Tenant '" + tenant + "' exists."); + } + + @Override + public String getContentType() { + return HttpConfigResponse.JSON_CONTENT_TYPE; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java new file mode 100644 index 00000000000..e373eb4478f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantHandler.java @@ -0,0 +1,96 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import java.util.List; +import java.util.concurrent.Executor; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.container.jdisc.HttpResponse; +import com.yahoo.container.logging.AccessLog; +import com.yahoo.yolean.Exceptions; +import com.yahoo.vespa.config.server.Tenant; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.http.BadRequestException; +import com.yahoo.vespa.config.server.http.HttpHandler; +import com.yahoo.vespa.config.server.http.InternalServerException; +import com.yahoo.vespa.config.server.http.Utils; +import com.yahoo.vespa.config.server.Tenants; + +/** + * Handler to create, get and delete a tenant. + * + * @author vegardh + */ +public class TenantHandler extends HttpHandler { + + private static final String TENANT_NAME_REGEXP = "[\\w-]+"; + private final Tenants tenants; + + public TenantHandler(Executor executor, AccessLog accessLog, Tenants tenants) { + super(executor, accessLog); + this.tenants = tenants; + } + + @Override + protected HttpResponse handlePUT(HttpRequest request) { + TenantName tenant = getAndValidateTenantFromRequest(request); + try { + tenants.createTenant(tenant); + } catch (Exception e) { + throw new InternalServerException(Exceptions.toMessageString(e)); + } + return new TenantCreateResponse(tenant); + } + + /** + * Gets the tenant name from the request, throws if it exists already and validates its name + * + * @param request an {@link com.yahoo.container.jdisc.HttpRequest} + * @return tenant name + */ + private TenantName getAndValidateTenantFromRequest(HttpRequest request) { + TenantName tenant = Utils.getTenantFromRequest(request); + Utils.checkThatTenantDoesNotExist(tenants, tenant); + validateTenantName(tenant); + return tenant; + } + + private void validateTenantName(TenantName tenant) { + if (!tenant.value().matches(TENANT_NAME_REGEXP)) { + throw new BadRequestException("Illegal tenant name: " + tenant); + } + } + + @Override + protected HttpResponse handleGET(HttpRequest request) { + TenantName tenant = getExistingTenant(request); + return new TenantGetResponse(tenant); + } + + @Override + protected HttpResponse handleDELETE(HttpRequest request) { + TenantName tenantName = getExistingTenant(request); + Tenant tenant = Utils.checkThatTenantExists(tenants, tenantName); + ApplicationRepo applicationRepo = tenant.getApplicationRepo(); + final List<ApplicationId> activeApplications = applicationRepo.listApplications(); + if (activeApplications.isEmpty()) { + try { + tenants.deleteTenant(tenantName); + } catch (Exception e) { + throw new InternalServerException(Exceptions.toMessageString(e)); + } + } else { + throw new BadRequestException("Cannot delete tenant '" + tenantName + "', as it has active applications: " + + activeApplications); + } + return new TenantDeleteResponse(tenantName); + } + + private TenantName getExistingTenant(HttpRequest request) { + TenantName tenant = Utils.getTenantFromRequest(request); + Utils.checkThatTenantExists(tenants, tenant); + return tenant; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java new file mode 100644 index 00000000000..5ba6d480871 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/http/v2/TenantRequest.java @@ -0,0 +1,15 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.http.v2; + +import com.yahoo.config.provision.ApplicationId; + +/** + * Config REST requests that have been bound to an application id + * + * @author vegardh + */ +public interface TenantRequest { + + ApplicationId getApplicationId(); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java new file mode 100644 index 00000000000..318a3f81d52 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/ElkProducer.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; +import com.yahoo.cloud.config.ElkConfig.Builder; + +import com.yahoo.cloud.config.ElkConfig; +import com.yahoo.vespa.defaults.Defaults; + +/** + * Produces the ELK config for the SuperModel + * + * @author vegardh + * @since 5.38 + * + */ +public class ElkProducer implements ElkConfig.Producer { + + private final ElkConfig config; + + public ElkProducer(ElkConfig config) { + this.config = config; + } + + @Override + public void getConfig(Builder builder) { + for (ElkConfig.Elasticsearch es : config.elasticsearch()) { + int port = es.port() != 0 ? es.port() : Defaults.getDefaults().vespaWebServicePort(); + builder.elasticsearch(new ElkConfig.Elasticsearch.Builder().host(es.host()).port(port)); + } + ElkConfig.Logstash.Builder logstashBuilder = new ElkConfig.Logstash.Builder(); + logstashBuilder. + config_file(Defaults.getDefaults().underVespaHome(config.logstash().config_file())). + source_field(config.logstash().source_field()). + spool_size(config.logstash().spool_size()); + ElkConfig.Logstash.Network.Builder networkBuilder = new ElkConfig.Logstash.Network.Builder(). + timeout(config.logstash().network().timeout()); + for (ElkConfig.Logstash.Network.Servers srv : config.logstash().network().servers()) { + networkBuilder. + servers(new ElkConfig.Logstash.Network.Servers.Builder(). + host(srv.host()). + port(srv.port())); + } + logstashBuilder.network(networkBuilder); + for (ElkConfig.Logstash.Files files : config.logstash().files()) { + logstashBuilder.files(new ElkConfig.Logstash.Files.Builder(). + paths(files.paths()). + fields(files.fields())); + } + builder.logstash(logstashBuilder); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java new file mode 100644 index 00000000000..d8f3c0da8d4 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/LbServicesProducer.java @@ -0,0 +1,117 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import com.google.common.base.Joiner; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.cloud.config.LbServicesConfig; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; + +/** + * Produces lb-services cfg + * + * @author vegardh + * @since 5.9 + */ +public class LbServicesProducer implements LbServicesConfig.Producer { + + private final Map<TenantName, Map<ApplicationId, Application>> models; + private final Zone zone; + + public LbServicesProducer(Map<TenantName, Map<ApplicationId, Application>> models, Zone zone) { + this.models = models; + this.zone = zone; + } + + @Override + public void getConfig(LbServicesConfig.Builder builder) { + models.keySet().stream() + .sorted() + .forEach(tenant -> { + builder.tenants(tenant.value(), getTenantConfig(models.get(tenant))); + }); + } + + private LbServicesConfig.Tenants.Builder getTenantConfig(Map<ApplicationId, Application> apps) { + LbServicesConfig.Tenants.Builder tb = new LbServicesConfig.Tenants.Builder(); + apps.keySet().stream() + .sorted() + .forEach(applicationId -> { + tb.applications(createLbAppIdKey(applicationId), getAppConfig(apps.get(applicationId))); + }); + return tb; + } + + private String createLbAppIdKey(ApplicationId applicationId) { + return applicationId.application().value() + ":" + zone.environment().value() + ":" + zone.region().value() + ":" + applicationId.instance().value(); + } + + private LbServicesConfig.Tenants.Applications.Builder getAppConfig(Application app) { + LbServicesConfig.Tenants.Applications.Builder ab = new LbServicesConfig.Tenants.Applications.Builder(); + ab.activeRotation(getActiveRotation(app)); + app.getModel().getHosts().stream() + .sorted((a, b) -> a.getHostname().compareTo(b.getHostname())) + .forEach(hostInfo -> { + ab.hosts(hostInfo.getHostname(), getHostsConfig(hostInfo)); + }); + return ab; + } + + private boolean getActiveRotation(Application app) { + boolean activeRotation = false; + for (HostInfo hostInfo : app.getModel().getHosts()) { + final Optional<ServiceInfo> container = hostInfo.getServices().stream().filter( + serviceInfo -> serviceInfo.getServiceType().equals("container") || + serviceInfo.getServiceType().equals("qrserver")). + findAny(); + if (container.isPresent()) { + activeRotation |= Boolean.valueOf(container.get().getProperty("activeRotation").get()); + } + } + return activeRotation; + } + + private LbServicesConfig.Tenants.Applications.Hosts.Builder getHostsConfig(HostInfo hostInfo) { + LbServicesConfig.Tenants.Applications.Hosts.Builder hb = new LbServicesConfig.Tenants.Applications.Hosts.Builder(); + hb.hostname(hostInfo.getHostname()); + hostInfo.getServices().stream() + .forEach(serviceInfo -> { + hb.services(serviceInfo.getServiceName(), getServiceConfig(serviceInfo)); + }); + return hb; + } + + private LbServicesConfig.Tenants.Applications.Hosts.Services.Builder getServiceConfig(ServiceInfo serviceInfo) { + final List<String> endpointAliases = Stream.of(serviceInfo.getProperty("endpointaliases").orElse("").split(",")). + filter(prop -> !"".equals(prop)).collect(Collectors.toList()); + endpointAliases.addAll(Stream.of(serviceInfo.getProperty("rotations").orElse("").split(",")).filter(prop -> !"".equals(prop)).collect(Collectors.toList())); + Collections.sort(endpointAliases); + + LbServicesConfig.Tenants.Applications.Hosts.Services.Builder sb = new LbServicesConfig.Tenants.Applications.Hosts.Services.Builder() + .type(serviceInfo.getServiceType()) + .clustertype(serviceInfo.getProperty("clustertype").orElse("")) + .clustername(serviceInfo.getProperty("clustername").orElse("")) + .configId(serviceInfo.getConfigId()) + .servicealiases(Stream.of(serviceInfo.getProperty("servicealiases").orElse("").split(",")). + filter(prop -> !"".equals(prop)).sorted((a, b) -> a.compareTo(b)).collect(Collectors.toList())) + .endpointaliases(endpointAliases) + .index(Integer.parseInt(serviceInfo.getProperty("index").orElse("999999"))); + serviceInfo.getPorts().stream() + .forEach(portInfo -> { + LbServicesConfig.Tenants.Applications.Hosts.Services.Ports.Builder pb = new LbServicesConfig.Tenants.Applications.Hosts.Services.Ports.Builder() + .number(portInfo.getPort()) + .tags(Joiner.on(" ").join(portInfo.getTags())); + sb.ports(pb); + }); + return sb; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java new file mode 100755 index 00000000000..6a63269ae6e --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/RoutingProducer.java @@ -0,0 +1,36 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; + +import com.yahoo.cloud.config.RoutingConfig; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.application.Application; + +import java.util.Map; + +/** + * Create global config based on info from the zone application + * + * @author can + * @since 5.60 + */ +public class RoutingProducer implements RoutingConfig.Producer { + + private final Map<TenantName, Map<ApplicationId, Application>> models; + + public RoutingProducer(Map<TenantName, Map<ApplicationId, Application>> models) { + this.models = models; + } + + @Override + public void getConfig(RoutingConfig.Builder builder) { + for (Map<ApplicationId, Application> model : models.values()) { + model.values().stream().filter(application -> application.getId().isHostedVespaRoutingApplication()).forEach(application -> { + for (HostInfo host : application.getModel().getHosts()) { + builder.hosts(host.getHostname()); + } + }); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java b/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java new file mode 100755 index 00000000000..2be7860b01f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/model/SuperModel.java @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.model; + +import com.yahoo.cloud.config.ElkConfig; +import com.yahoo.cloud.config.LbServicesConfig; +import com.yahoo.cloud.config.RoutingConfig; +import com.yahoo.config.ConfigInstance; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.server.application.Application; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * A config model that spans across all applications of all tenants in the config server. + * + * @author vegardh + * @since 5.9 + * + */ +public class SuperModel implements LbServicesConfig.Producer, ElkConfig.Producer, RoutingConfig.Producer { + + private final Map<TenantName, Map<ApplicationId, Application>> models; + private final LbServicesProducer lbProd; + private final ElkProducer elkProd; + private final RoutingProducer zoneProd; + + public SuperModel(Map<TenantName, Map<ApplicationId, Application>> newModels, ElkConfig elkConfig, Zone zone) { + this.models = newModels; + this.lbProd = new LbServicesProducer(Collections.unmodifiableMap(models), zone); + this.elkProd = new ElkProducer(elkConfig); + this.zoneProd = new RoutingProducer(Collections.unmodifiableMap(models)); + } + + public ConfigPayload getConfig(ConfigKey<?> configKey) throws IOException { + // TODO: Override not applied, but not really necessary here + if (configKey.equals(new ConfigKey<>(LbServicesConfig.class, configKey.getConfigId()))) { + LbServicesConfig.Builder builder = new LbServicesConfig.Builder(); + getConfig(builder); + return ConfigPayload.fromInstance(new LbServicesConfig(builder)); + } else if (configKey.equals(new ConfigKey<>(ElkConfig.class, configKey.getConfigId()))) { + ElkConfig.Builder builder = new ElkConfig.Builder(); + getConfig(builder); + return ConfigPayload.fromInstance(new ElkConfig(builder)); + } else if (configKey.equals(new ConfigKey<>(RoutingConfig.class, configKey.getConfigId()))) { + RoutingConfig.Builder builder = new RoutingConfig.Builder(); + getConfig(builder); + return ConfigPayload.fromInstance(new RoutingConfig(builder)); + } else { + return null; + } + } + + public Map<TenantName, Map<ApplicationId, Application>> getCurrentModels() { + return models; + } + + @Override + public void getConfig(LbServicesConfig.Builder builder) { + lbProd.getConfig(builder); + } + + @Override + public void getConfig(com.yahoo.cloud.config.ElkConfig.Builder builder) { + elkProd.getConfig(builder); + } + + @Override + public void getConfig(RoutingConfig.Builder builder) { + zoneProd.getConfig(builder); + } + + public <CONFIGTYPE extends ConfigInstance> CONFIGTYPE getConfig(Class<CONFIGTYPE> configClass, ApplicationId applicationId, String configId) throws IOException { + TenantName tenant = applicationId.tenant(); + if (!models.containsKey(tenant)) { + throw new IllegalArgumentException("Tenant " + tenant + " not found"); + } + Map<ApplicationId, Application> applications = models.get(tenant); + if (!applications.containsKey(applicationId)) { + throw new IllegalArgumentException("Application id " + applicationId + " not found"); + } + Application application = applications.get(applicationId); + ConfigKey<CONFIGTYPE> key = new ConfigKey<>(configClass, configId); + ConfigPayload payload = application.getModel().getConfig(key, (ConfigDefinition)null, null); + return payload.toInstance(configClass, configId); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java new file mode 100644 index 00000000000..c3046887b0e --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ActivatedModelsBuilder.java @@ -0,0 +1,126 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.modelfactory; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.model.application.provider.MockFileRegistry; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.config.provision.Zone; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.RotationsCache; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.Tenants; +import com.yahoo.vespa.config.server.application.Application; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.deploy.ModelContextImpl; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.monitoring.Metrics; +import com.yahoo.vespa.config.server.provision.StaticProvisioner; +import com.yahoo.vespa.config.server.session.SessionZooKeeperClient; +import com.yahoo.vespa.config.server.session.SilentDeployLogger; +import com.yahoo.vespa.curator.Curator; + +import java.util.Map; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * Builds activated versions of the right model versions + * + * @author bratseth + */ +public class ActivatedModelsBuilder extends ModelsBuilder<Application> { + + private static final Logger log = Logger.getLogger(ActivatedModelsBuilder.class.getName()); + + private final TenantName tenant; + private final long appGeneration; + private final SessionZooKeeperClient zkClient; + private final Optional<PermanentApplicationPackage> permanentApplicationPackage; + private final Optional<com.yahoo.config.provision.Provisioner> hostProvisioner; + private final ConfigserverConfig configserverConfig; + private final ConfigDefinitionRepo configDefinitionRepo; + private final Metrics metrics; + private final Curator curator; + private final Zone zone; + private final DeployLogger logger; + + public ActivatedModelsBuilder(TenantName tenant, long appGeneration, SessionZooKeeperClient zkClient, GlobalComponentRegistry globalComponentRegistry) { + super(globalComponentRegistry.getModelFactoryRegistry()); + this.tenant = tenant; + this.appGeneration = appGeneration; + this.zkClient = zkClient; + this.permanentApplicationPackage = Optional.of(globalComponentRegistry.getPermanentApplicationPackage()); + this.configserverConfig = globalComponentRegistry.getConfigserverConfig(); + this.configDefinitionRepo = globalComponentRegistry.getConfigDefinitionRepo(); + this.metrics = globalComponentRegistry.getMetrics(); + this.hostProvisioner = globalComponentRegistry.getHostProvisioner(); + this.curator = globalComponentRegistry.getCurator(); + this.zone = globalComponentRegistry.getZone(); + this.logger = new SilentDeployLogger(); + } + + @Override + protected Application buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage, + ApplicationId applicationId) { + Version version = modelFactory.getVersion(); + log.log(LogLevel.DEBUG, String.format("Loading model version %s for session %s application %s", + version, appGeneration, applicationId)); + ModelContext modelContext = new ModelContextImpl( + applicationPackage, + Optional.<Model>empty(), + permanentApplicationPackage.get().applicationPackage(), + logger, + configDefinitionRepo, + getForVersionOrLatest(applicationPackage.getFileRegistryMap(), modelFactory.getVersion()).orElse(new MockFileRegistry()), + createHostProvisioner(getForVersionOrLatest(applicationPackage.getProvisionInfoMap(), modelFactory.getVersion())), + createModelContextProperties(applicationId), + Optional.empty(), + Optional.empty()); + ServerCache cache = zkClient.loadServerCache(); + MetricUpdater applicationMetricUpdater = metrics.getOrCreateMetricUpdater(Metrics.createDimensions(applicationId)); + return new Application(modelFactory.createModel(modelContext), cache, appGeneration, version, + applicationMetricUpdater, applicationId); + } + + private Optional<HostProvisioner> createHostProvisioner(Optional<ProvisionInfo> provisionInfo) { + if (hostProvisioner.isPresent() && provisionInfo.isPresent()) { + return Optional.of(createStaticProvisioner(provisionInfo.get())); + } + return Optional.empty(); + } + + private HostProvisioner createStaticProvisioner(ProvisionInfo provisionInfo) { + return new StaticProvisioner(provisionInfo); + } + + private static <T> Optional<T> getForVersionOrLatest(Map<Version, T> map, Version version) { + if (map.isEmpty()) { + return Optional.empty(); + } + T value = map.get(version); + if (value == null) { + value = map.get(map.keySet().stream().max((a, b) -> a.compareTo(b)).get()); + } + return Optional.of(value); + } + + private ModelContext.Properties createModelContextProperties(ApplicationId applicationId) { + return createModelContextProperties( + applicationId, + configserverConfig, + zone, + new RotationsCache(curator, Tenants.getTenantPath(tenant)).readRotationsFromZooKeeper(applicationId)); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java new file mode 100644 index 00000000000..a18a200f39e --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelFactoryRegistry.java @@ -0,0 +1,58 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.modelfactory; + +import com.google.inject.Inject; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.config.server.http.UnknownVespaVersionException; + +import java.util.*; + +/** + * A registry of model factories. Allows querying for a specific version of a {@link ModelFactory} or + * simply returning all of them. Keeps track of the latest {@link com.yahoo.config.provision.Version} supported. + * + * @author lulf + */ +public class ModelFactoryRegistry { + + private final Map<Version, ModelFactory> factories = new HashMap<>(); + + @Inject + public ModelFactoryRegistry(ComponentRegistry<ModelFactory> factories) { + this(factories.allComponents()); + } + + public ModelFactoryRegistry(List<ModelFactory> modelFactories) { + if (modelFactories.isEmpty()) { + throw new IllegalArgumentException("No ModelFactory instances registered, cannot build config models"); + } + for (ModelFactory factory : modelFactories) { + factories.put(factory.getVersion(), factory); + } + } + + public Set<Version> allVersions() { return factories.keySet(); } + + /** + * Returns the factory for the given version + * + * @throws UnknownVespaVersionException if there is no factory for this version + */ + public ModelFactory getFactory(Version version) { + if ( ! factories.containsKey(version)) + throw new UnknownVespaVersionException("Unknown Vespa version '" + version + "', cannot build config model for this version"); + return factories.get(version); + } + + /** + * Return all factories that can build a model. + * + * @return An immutable collection of {@link com.yahoo.config.model.api.ModelFactory} instances. + */ + public Collection<ModelFactory> getFactories() { + return Collections.unmodifiableCollection(factories.values()); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java new file mode 100644 index 00000000000..9c9ec160a95 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelResult.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.modelfactory; + +import com.yahoo.config.model.api.Model; + +/** + * @author bratseth + */ +public interface ModelResult { + + Model getModel(); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java new file mode 100644 index 00000000000..99036ee0027 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/ModelsBuilder.java @@ -0,0 +1,132 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.modelfactory; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.Version; +import com.yahoo.config.provision.Zone; +import com.yahoo.vespa.config.server.ConfigServerSpec; +import com.yahoo.vespa.config.server.deploy.ModelContextImpl; +import com.yahoo.vespa.config.server.http.UnknownVespaVersionException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Responsible for building the right versions of application models for a given tenant and application generation. + * Actual model building is implemented by subclasses because it differs in the prepare and activate phases. + * + * @author bratseth + */ +public abstract class ModelsBuilder<MODELRESULT extends ModelResult> { + + private static final Logger log = Logger.getLogger(ModelsBuilder.class.getName()); + + private final ModelFactoryRegistry modelFactoryRegistry; + + protected ModelsBuilder(ModelFactoryRegistry modelFactoryRegistry) { + this.modelFactoryRegistry = modelFactoryRegistry; + } + + public List<MODELRESULT> buildModels(ApplicationId applicationId, ApplicationPackage applicationPackage) { + Set<Version> versions = modelFactoryRegistry.allVersions(); + + // If the application specifies a major, load models only for that + Optional<Integer> requestedMajorVersion = applicationPackage.getMajorVersion(); + if (requestedMajorVersion.isPresent()) + versions = filterByMajorVersion(requestedMajorVersion.get(), versions); + + // Load models by one major version at the time as new major versions are allowed to be unloadable + // in the case where an existing application is incompatible with a new major version + // (which is possible by the definition of major) + List<Integer> majorVersions = versions.stream() + .map(Version::getMajor) + .distinct() + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + List<MODELRESULT> allApplicationModels = new ArrayList<>(); + for (int i = 0; i < majorVersions.size(); i++) { + try { + allApplicationModels.addAll(buildModelVersion(filterByMajorVersion(majorVersions.get(i), versions), + applicationId, applicationPackage)); + + // skip old config models after we have found a major version which works + if (allApplicationModels.size() > 0 && allApplicationModels.get(0).getModel().skipOldConfigModels()) + break; + } + catch (RuntimeException e) { // TODO: Make this a specialized exception + boolean isOldestMajor = i == majorVersions.size() - 1; + if (isOldestMajor) { + if (e instanceof NoSuchElementException && "No value present".equals(e.getMessage())) { + e.printStackTrace(); + } + throw new IllegalArgumentException(applicationId + ": Error loading model", e); + } else { + log.log(Level.INFO, applicationId + ": Skipping major version " + majorVersions.get(i), e); + } + } + } + return allApplicationModels; + } + + private List<MODELRESULT> buildModelVersion(Set<Version> versions, ApplicationId applicationId, + ApplicationPackage applicationPackage) { + Version latest = findLatest(versions); + // load latest application version + MODELRESULT latestApplicationVersion = buildModelVersion(modelFactoryRegistry.getFactory(latest), applicationPackage, applicationId); + if (latestApplicationVersion.getModel().skipOldConfigModels()) { + return Collections.singletonList(latestApplicationVersion); + } + else { // load old model versions + List<MODELRESULT> allApplicationVersions = new ArrayList<>(); + allApplicationVersions.add(latestApplicationVersion); + for (Version version : versions) { + if (version.equals(latest)) continue; // already loaded + allApplicationVersions.add(buildModelVersion(modelFactoryRegistry.getFactory(version), applicationPackage, applicationId)); + } + return allApplicationVersions; + } + } + + private Set<Version> filterByMajorVersion(int majorVersion, Set<Version> versions) { + Set<Version> filteredVersions = versions.stream().filter(v -> v.getMajor() == majorVersion).collect(Collectors.toSet()); + if (filteredVersions.isEmpty()) + throw new UnknownVespaVersionException("No Vespa versions matching major version " + majorVersion + " are present"); + return filteredVersions; + } + + private Version findLatest(Set<Version> versionSet) { + List<Version> versionList = new ArrayList<>(versionSet); + Collections.sort(versionList); + return versionList.get(versionList.size() - 1); + } + + protected abstract MODELRESULT buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage, + ApplicationId applicationId); + + protected ModelContext.Properties createModelContextProperties(ApplicationId applicationId, + ConfigserverConfig configserverConfig, + Zone zone, + Set<Rotation> rotations) { + return new ModelContextImpl.Properties( + applicationId, + configserverConfig.multitenant(), + ConfigServerSpec.fromConfig(configserverConfig), + configserverConfig.hostedVespa(), + zone, + rotations); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java new file mode 100644 index 00000000000..0d6170909a7 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/modelfactory/PreparedModelsBuilder.java @@ -0,0 +1,224 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.modelfactory; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.ConfigChangeAction; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.model.api.ModelCreateResult; +import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.model.application.provider.FilesApplicationPackage; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.Version; +import com.yahoo.config.provision.Zone; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.ConfigServerSpec; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.HostValidator; +import com.yahoo.vespa.config.server.RotationsCache; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.deploy.ModelContextImpl; +import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; +import com.yahoo.vespa.config.server.provision.ProvisionerAdapter; +import com.yahoo.vespa.config.server.session.FileDistributionFactory; +import com.yahoo.vespa.config.server.session.PrepareParams; +import com.yahoo.vespa.config.server.session.SessionContext; +import com.yahoo.vespa.curator.Curator; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * @author bratseth + */ +public class PreparedModelsBuilder extends ModelsBuilder<PreparedModelsBuilder.PreparedModelResult> { + + private static final Logger log = Logger.getLogger(PreparedModelsBuilder.class.getName()); + + private final PermanentApplicationPackage permanentApplicationPackage; + private final ConfigserverConfig configserverConfig; + private final ConfigDefinitionRepo configDefinitionRepo; + private final Curator curator; + private final Zone zone; + private final SessionContext context; + private final DeployLogger logger; + private final PrepareParams params; + private final FileDistributionFactory fileDistributionFactory; + private final HostProvisionerProvider hostProvisionerProvider; + private final Optional<ApplicationSet> currentActiveApplicationSet; + private final ApplicationId applicationId; + private final RotationsCache rotationsCache; + private final Set<Rotation> rotations; + private final ModelContext.Properties properties; + + /** Construct from global component registry */ + public PreparedModelsBuilder(GlobalComponentRegistry globalComponentRegistry, + FileDistributionFactory fileDistributionFactory, + HostProvisionerProvider hostProvisionerProvider, + SessionContext context, + DeployLogger logger, + PrepareParams params, + Optional<ApplicationSet> currentActiveApplicationSet, + Path tenantPath) { + super(globalComponentRegistry.getModelFactoryRegistry()); + this.permanentApplicationPackage = globalComponentRegistry.getPermanentApplicationPackage(); + this.configserverConfig = globalComponentRegistry.getConfigserverConfig(); + this.configDefinitionRepo = globalComponentRegistry.getConfigDefinitionRepo(); + this.curator = globalComponentRegistry.getCurator(); + this.zone = globalComponentRegistry.getZone(); + + this.fileDistributionFactory = fileDistributionFactory; + this.hostProvisionerProvider = hostProvisionerProvider; + + this.context = context; + this.logger = logger; + this.params = params; + this.currentActiveApplicationSet = currentActiveApplicationSet; + + this.applicationId = params.getApplicationId(); + this.rotationsCache = new RotationsCache(curator, tenantPath); + this.rotations = getRotations(params.rotations()); + this.properties = createModelContextProperties( + params.getApplicationId(), + configserverConfig, + zone, + rotations); + } + + /** Construct with all dependencies passed separately */ + public PreparedModelsBuilder(ModelFactoryRegistry modelFactoryRegistry, + PermanentApplicationPackage permanentApplicationPackage, + ConfigserverConfig configserverConfig, + ConfigDefinitionRepo configDefinitionRepo, + Curator curator, + Zone zone, + FileDistributionFactory fileDistributionFactory, + HostProvisionerProvider hostProvisionerProvider, + SessionContext context, + DeployLogger logger, + PrepareParams params, + Optional<ApplicationSet> currentActiveApplicationSet, + Path tenantPath) { + super(modelFactoryRegistry); + this.permanentApplicationPackage = permanentApplicationPackage; + this.configserverConfig = configserverConfig; + this.configDefinitionRepo = configDefinitionRepo; + this.curator = curator; + this.zone = zone; + + this.fileDistributionFactory = fileDistributionFactory; + this.hostProvisionerProvider = hostProvisionerProvider; + + this.context = context; + this.logger = logger; + this.params = params; + this.currentActiveApplicationSet = currentActiveApplicationSet; + + this.applicationId = params.getApplicationId(); + this.rotationsCache = new RotationsCache(curator, tenantPath); + this.rotations = getRotations(params.rotations()); + this.properties = new ModelContextImpl.Properties( + params.getApplicationId(), + configserverConfig.multitenant(), + ConfigServerSpec.fromConfig(configserverConfig), + configserverConfig.hostedVespa(), + zone, + rotations); + } + + @Override + protected PreparedModelResult buildModelVersion(ModelFactory modelFactory, ApplicationPackage applicationPackage, + ApplicationId applicationId) { + Version version = modelFactory.getVersion(); + log.log(LogLevel.DEBUG, "Start building model for Vespa version " + version); + FileDistributionProvider fileDistributionProvider = fileDistributionFactory.createProvider( + context.getServerDBSessionDir(), + applicationId); + + Optional<HostProvisioner> hostProvisioner = createHostProvisionerAdapter(properties); + Optional<Model> previousModel = currentActiveApplicationSet + .map(set -> set.getForVersionOrLatest(Optional.of(version)).getModel()); + ModelContext modelContext = new ModelContextImpl( + applicationPackage, + previousModel, + permanentApplicationPackage.applicationPackage(), + logger, + configDefinitionRepo, + fileDistributionProvider.getFileRegistry(), + hostProvisioner, + properties, + getAppDir(applicationPackage), + Optional.of(version)); + + log.log(LogLevel.DEBUG, "Running createAndValidateModel for Vespa version " + version); + ModelCreateResult result = modelFactory.createAndValidateModel(modelContext, params.ignoreValidationErrors()); + validateModelHosts(context.getHostValidator(), applicationId, result.getModel()); + log.log(LogLevel.DEBUG, "Done building model for Vespa version " + version); + return new PreparedModelsBuilder.PreparedModelResult(version, result.getModel(), fileDistributionProvider, result.getConfigChangeActions()); + } + + private Optional<File> getAppDir(ApplicationPackage applicationPackage) { + try { + return applicationPackage instanceof FilesApplicationPackage ? + Optional.of(((FilesApplicationPackage) applicationPackage).getAppDir()) : + Optional.empty(); + } catch (IOException e) { + throw new RuntimeException("Could not find app dir", e); + } + } + + private void validateModelHosts(HostValidator<ApplicationId> hostValidator, ApplicationId applicationId, Model model) { + hostValidator.verifyHosts(applicationId, model.getHosts().stream().map(hostInfo -> hostInfo.getHostname()) + .collect(Collectors.toList())); + } + + private Set<Rotation> getRotations(Set<Rotation> rotations) { + if (rotations == null || rotations.isEmpty()) { + rotations = rotationsCache.readRotationsFromZooKeeper(applicationId); + } + return rotations; + } + + private Optional<HostProvisioner> createHostProvisionerAdapter(ModelContext.Properties properties) { + return hostProvisionerProvider.getHostProvisioner().map( + provisioner -> new ProvisionerAdapter(provisioner, properties.applicationId())); + } + + + /** The result of preparing a single model version */ + public static class PreparedModelResult implements ModelResult { + + public final Version version; + public final Model model; + public final FileDistributionProvider fileDistributionProvider; + public final List<ConfigChangeAction> actions; + + public PreparedModelResult(Version version, Model model, + FileDistributionProvider fileDistributionProvider, List<ConfigChangeAction> actions) { + this.version = version; + this.model = model; + this.fileDistributionProvider = fileDistributionProvider; + this.actions = actions; + } + + @Override + public Model getModel() { + return model; + } + + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java new file mode 100644 index 00000000000..8eae14b9aac --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdater.java @@ -0,0 +1,222 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.monitoring; + +import com.yahoo.jdisc.Metric; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.server.RequestHandler; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.yahoo.vespa.config.server.monitoring.Metrics.getMetricName; +/** + * @author musum + */ +// TODO javadoc, thread non-safeness maybe +public class MetricUpdater { + private static final String METRIC_UNKNOWN_HOSTS = getMetricName("unknownHostRequests"); + private static final String METRIC_SESSION_CHANGE_ERRORS = getMetricName("sessionChangeErrors"); + private static final String METRIC_NEW_SESSIONS = getMetricName("newSessions"); + private static final String METRIC_PREPARED_SESSIONS = getMetricName("preparedSessions"); + private static final String METRIC_ACTIVATED_SESSIONS = getMetricName("activeSessions"); + private static final String METRIC_DEACTIVATED_SESSIONS = getMetricName("inactiveSessions"); + private static final String METRIC_ADDED_SESSIONS = getMetricName("addedSessions"); + private static final String METRIC_REMOVED_SESSIONS = getMetricName("removedSessions"); + private static final String METRIC_ZK_CONNECTION_LOST = getMetricName("zkConnectionLost"); + private static final String METRIC_ZK_RECONNECTED = getMetricName("zkReconnected"); + private static final String METRIC_ZK_CONNECTED = getMetricName("zkConnected"); + private static final String METRIC_ZK_SUSPENDED = getMetricName("zkSuspended"); + private static final String METRIC_TENANTS = getMetricName("tenants"); + private static final String METRIC_HOSTS = getMetricName("hosts"); + private static final String METRIC_APPLICATIONS = getMetricName("applications"); + private static final String METRIC_CACHE_CONFIG_ELEMENTS = getMetricName("cacheConfigElems"); + private static final String METRIC_CACHE_CONFIG_CHECKSUMS = getMetricName("cacheChecksumElems"); + private static final String METRIC_DELAYED_RESPONSES = getMetricName("delayedResponses"); + private static final String METRIC_RPCSERVER_WORK_QUEUE_SIZE = getMetricName("rpcServerWorkQueueSize"); + + + private final Metrics metrics; + private final Map<String, String> dimensions; + private final Metric.Context metricContext; + private final Map<String, Number> staticMetrics = new ConcurrentHashMap<>(); + + public MetricUpdater(Metrics metrics, Map<String, String> dimensions) { + this.metrics = metrics; + this.dimensions = dimensions; + metricContext = createContext(metrics, dimensions); + } + + public void incrementRequests() { + metrics.incrementRequests(metricContext); + } + + public void incrementFailedRequests() { + metrics.incrementFailedRequests(metricContext); + } + + public void incrementProcTime(long increment) { + metrics.incrementProcTime(increment, metricContext); + } + + /** + * Sets the count for number of config elements in the {@link ServerCache} + * + * @param elems number of elements + */ + public void setCacheConfigElems(long elems) { + staticMetrics.put(METRIC_CACHE_CONFIG_ELEMENTS, elems); + } + + /** + * Sets the count for number of checksum elements in the {@link ServerCache} + * + * @param elems number of elements + */ + public void setCacheChecksumElems(long elems) { + staticMetrics.put(METRIC_CACHE_CONFIG_CHECKSUMS, elems); + } + + /** + * Sets the number of outstanding responses (unchanged config in long poll) + * + * @param elems number of elements + */ + public void setDelayedResponses(long elems) { + staticMetrics.put(METRIC_DELAYED_RESPONSES, elems); + } + + private void setStaticMetric(String name, int size) { + staticMetrics.put(name, size); + } + + /** + * Increment the number of requests where we were unable to map host to a {@link RequestHandler}. + */ + public void incUnknownHostRequests() { + metrics.increment(METRIC_UNKNOWN_HOSTS, metricContext); + } + + private Metric.Context createContext(Metrics metrics, Map<String, String> dimensions) { + if (metrics == null) return null; + + return metrics.getMetric().createContext(dimensions); + } + + public Map<String, Number> getStaticMetrics() { + return staticMetrics; + } + + public Metric.Context getMetricContext() { + return metricContext; + } + + public Map<String, String> getDimensions() { + return dimensions; + } + + /** + * Increment the number of errors from changed sessions. + */ + public void incSessionChangeErrors() { + metrics.increment(METRIC_SESSION_CHANGE_ERRORS, metricContext); + } + + /** + * Set the number of new sessions. + */ + public void setNewSessions(int numNew) { + setStaticMetric(METRIC_NEW_SESSIONS, numNew); + } + + /** + * Set the number of prepared sessions. + */ + public void setPreparedSessions(int numPrepared) { + setStaticMetric(METRIC_PREPARED_SESSIONS, numPrepared); + } + + /** + * Set the number of activated sessions. + */ + public void setActivatedSessions(int numActivated) { + setStaticMetric(METRIC_ACTIVATED_SESSIONS, numActivated); + } + + /** + * Set the number of deactivated sessions. + */ + public void setDeactivatedSessions(int numDeactivated) { + setStaticMetric(METRIC_DEACTIVATED_SESSIONS, numDeactivated); + } + + /** + * Increment the number of removed sessions. + */ + public void incRemovedSessions() { + metrics.increment(METRIC_REMOVED_SESSIONS, metricContext); + } + + /** + * Increment the number of added sessions. + */ + public void incAddedSessions() { + metrics.increment(METRIC_ADDED_SESSIONS, metricContext); + } + + public static MetricUpdater createTestUpdater() { + return new MetricUpdater(Metrics.createTestMetrics(), null); + } + + /** + * Increment the number of ZK connection losses. + */ + public void incZKConnectionLost() { + metrics.increment(METRIC_ZK_CONNECTION_LOST, metricContext); + } + + /** + * Increment the number of ZK connection establishments. + */ + public void incZKConnected() { + metrics.increment(METRIC_ZK_CONNECTED, metricContext); + } + + /** + * Increment the number of ZK connection suspended. + */ + public void incZKSuspended() { + metrics.increment(METRIC_ZK_SUSPENDED, metricContext); + } + + /** + * Increment the number of ZK reconnections. + */ + public void incZKReconnected() { + metrics.increment(METRIC_ZK_RECONNECTED, metricContext); + } + + /** + * Set the number of tenants. + */ + public void setTenants(int numTenants) { + setStaticMetric(METRIC_TENANTS, numTenants); + } + + /** + * Set the number of hosts. + */ + public void setHosts(int numHosts) { + setStaticMetric(METRIC_HOSTS, numHosts); + } + + /** + * Set the number of applications. + */ + public void setApplications(int numApplications) { + setStaticMetric(METRIC_APPLICATIONS, numApplications); + } + + public void setRpcServerQueueSize(int numQueued) { + metrics.set(METRIC_RPCSERVER_WORK_QUEUE_SIZE, numQueued, metricContext); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java new file mode 100644 index 00000000000..7f40414c147 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/MetricUpdaterFactory.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.monitoring; + +import java.util.Map; + +/** + * A factory for creating metric updates with a given context. + * + * @author lulf + * @since 5.15 + */ +public interface MetricUpdaterFactory { + MetricUpdater getOrCreateMetricUpdater(Map<String, String> dimensions); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java new file mode 100644 index 00000000000..d0984baefc2 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/monitoring/Metrics.java @@ -0,0 +1,140 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.monitoring; + +import com.google.inject.Inject; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.container.jdisc.config.HealthMonitorConfig; +import com.yahoo.docproc.jdisc.metric.NullMetric; +import com.yahoo.jdisc.Metric; +import com.yahoo.log.LogLevel; +import com.yahoo.statistics.Statistics; +import com.yahoo.statistics.Counter; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Statistics for server. The statistics framework takes care of logging. + * + * @author Harald Musum + * @since 4.2 + */ +public class Metrics extends TimerTask implements MetricUpdaterFactory { + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(Metrics.class.getName()); + private static final String METRIC_REQUESTS = getMetricName("requests"); + private static final String METRIC_FAILED_REQUESTS = getMetricName("failedRequests"); + private static final String METRIC_FREE_MEMORY = getMetricName("freeMemory"); + private static final String METRIC_LATENCY = getMetricName("latency"); + + private final Counter requests; + private final Counter failedRequests; + private final Counter procTimeCounter; + private final Metric metric; + + // TODO The map is the key for now + private final Map<Map<String, String>, MetricUpdater> metricUpdaters = new ConcurrentHashMap<>(); + private final Timer timer = new Timer(); + + @Inject + public Metrics(Metric metric, Statistics statistics, HealthMonitorConfig healthMonitorConfig) { + this.metric = metric; + requests = createCounter(METRIC_REQUESTS, statistics); + failedRequests = createCounter(METRIC_FAILED_REQUESTS, statistics); + procTimeCounter = createCounter("procTime", statistics); + timer.scheduleAtFixedRate(this, 5000, (long) (healthMonitorConfig.snapshot_interval() * 1000)); + log.log(LogLevel.DEBUG, "Metric update interval is " + healthMonitorConfig.snapshot_interval() + " seconds"); + } + + public static Metrics createTestMetrics() { + NullMetric metric = new NullMetric(); + Statistics.NullImplementation statistics = new Statistics.NullImplementation(); + HealthMonitorConfig.Builder builder = new HealthMonitorConfig.Builder(); + builder.snapshot_interval(60.0); + return new Metrics(metric, statistics, new HealthMonitorConfig(builder)); + } + + private Counter createCounter(String name, Statistics statistics) { + return new Counter(name, statistics, false); + } + + + void incrementRequests(Metric.Context metricContext) { + requests.increment(1); + metric.add(METRIC_REQUESTS, 1, metricContext); + } + + void incrementFailedRequests(Metric.Context metricContext) { + failedRequests.increment(1); + metric.add(METRIC_FAILED_REQUESTS, 1, metricContext); + } + + void incrementProcTime(long increment, Metric.Context metricContext) { + procTimeCounter.increment(increment); + metric.set(METRIC_LATENCY, increment, metricContext); + } + + public long getRequests() { + return requests.get(); + } + + public Metric getMetric() { + return metric; + } + + public MetricUpdater removeMetricUpdater(Map<String, String> dimensions) { + return metricUpdaters.remove(dimensions); + } + + public static Map<String, String> createDimensions(ApplicationId applicationId) { + final Map<String, String> properties = new LinkedHashMap<>(); + properties.put("tenantName", applicationId.tenant().value()); + properties.put("applicationName", applicationId.application().value()); + properties.put("applicationInstance", applicationId.instance().value()); + return properties; + } + + public static Map<String, String> createDimensions(TenantName tenant) { + final Map<String, String> properties = new LinkedHashMap<>(); + properties.put("tenantName", tenant.value()); + return properties; + } + + public synchronized MetricUpdater getOrCreateMetricUpdater(Map<String, String> dimensions) { + if (metricUpdaters.containsKey(dimensions)) { + return metricUpdaters.get(dimensions); + } + MetricUpdater metricUpdater = new MetricUpdater(this, dimensions); + metricUpdaters.put(dimensions, metricUpdater); + return metricUpdater; + } + + @Override + public void run() { + for (MetricUpdater metricUpdater : metricUpdaters.values()) { + log.log(LogLevel.DEBUG, "Running metric updater for static values for " + metricUpdater.getDimensions()); + for (Map.Entry<String, Number> fixedMetric : metricUpdater.getStaticMetrics().entrySet()) { + log.log(LogLevel.DEBUG, "Setting " + fixedMetric.getKey()); + metric.set(fixedMetric.getKey(), fixedMetric.getValue(), metricUpdater.getMetricContext()); + } + } + setRegularMetrics(); + timer.purge(); + } + + private void setRegularMetrics() { + metric.set(METRIC_FREE_MEMORY, Runtime.getRuntime().freeMemory(), null); + } + + void increment(String metricName, Metric.Context context) { + metric.add(metricName, 1, context); + } + + void set(String metricName, Number value, Metric.Context context) { + metric.set(metricName, value, context); + } + + static String getMetricName(String name) { + return "configserver." + name; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java new file mode 100644 index 00000000000..91429eaefc0 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/HostProvisionerProvider.java @@ -0,0 +1,62 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.provision; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.component.ComponentId; +import com.yahoo.component.provider.ComponentRegistry; +import com.yahoo.config.provision.Provisioner; +import com.yahoo.log.LogLevel; + +import java.util.Optional; +import java.util.logging.Logger; + +/** + * This class is necessary to support both having and not having a host provisioner. We inject + * a component registry here, which then enables us to check whether or not we have a provisioner available. + * + * @author lulf + * @since 5.15 + */ +public class HostProvisionerProvider { + + private static final Logger log = Logger.getLogger(HostProvisionerProvider.class.getName()); + private final Optional<Provisioner> hostProvisioner; + + public HostProvisionerProvider(ComponentRegistry<Provisioner> hostProvisionerRegistry, ConfigserverConfig configserverConfig) { + if (hostProvisionerRegistry.allComponents().isEmpty() || !configserverConfig.hostedVespa()) { + hostProvisioner = Optional.empty(); + } else { + log.log(LogLevel.DEBUG, "Host provisioner injected. Will be used for all deployments"); + hostProvisioner = Optional.of(hostProvisionerRegistry.allComponents().get(0)); + } + } + + private HostProvisionerProvider(ComponentRegistry<Provisioner> componentRegistry) { + this(componentRegistry, new ConfigserverConfig(new ConfigserverConfig.Builder())); + } + + public Optional<Provisioner> getHostProvisioner() { + return hostProvisioner; + } + + // for testing + public static HostProvisionerProvider empty() { + return new HostProvisionerProvider(new ComponentRegistry<>()); + } + + // for testing + public static HostProvisionerProvider withProvisioner(Provisioner provisioner) { + ComponentRegistry<Provisioner> registry = new ComponentRegistry<>(); + registry.register(ComponentId.createAnonymousComponentId("foobar"), provisioner); + return new HostProvisionerProvider(registry, new ConfigserverConfig(new ConfigserverConfig.Builder().hostedVespa(true))); + } + + /** Creates either an empty provider or a provider having the given provisioner */ + public static HostProvisionerProvider from(Optional<Provisioner> provisioner) { + if (provisioner.isPresent()) + return withProvisioner(provisioner.get()); + else + return empty(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java new file mode 100644 index 00000000000..c1884278cd4 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/ProvisionerAdapter.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.provision; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.provision.*; +import com.yahoo.config.provision.Provisioner; + +import java.util.*; + +/** + * A wrapper for {@link Provisioner} to avoid having to expose multitenant + * behavior to the config model. Adapts interface from a {@link HostProvisioner} to a + * {@link Provisioner}. + * + * @author lulf + * @since 5.11 + */ +public class ProvisionerAdapter implements HostProvisioner { + + private final Provisioner provisioner; + private final ApplicationId applicationId; + + public ProvisionerAdapter(Provisioner provisioner, ApplicationId applicationId) { + this.provisioner = provisioner; + this.applicationId = applicationId; + } + + @Override + public HostSpec allocateHost(String alias) { + throw new UnsupportedOperationException("Allocating a single host in a hosted environment is not possible"); + } + + @Override + public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) { + return provisioner.prepare(applicationId, cluster, capacity, groups, logger); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java new file mode 100644 index 00000000000..0c9575fd834 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/provision/StaticProvisioner.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.provision; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.api.HostProvisioner; +import com.yahoo.config.provision.*; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Host provisioning from an existing {@link ProvisionInfo} instance. + * + * @author bratseth + */ +public class StaticProvisioner implements HostProvisioner { + + private final ProvisionInfo provisionInfo; + + public StaticProvisioner(ProvisionInfo provisionInfo) { + this.provisionInfo = provisionInfo; + } + + @Override + public HostSpec allocateHost(String alias) { + throw new UnsupportedOperationException(); + } + + @Override + public List<HostSpec> prepare(ClusterSpec cluster, Capacity capacity, int groups, ProvisionLogger logger) { + List<HostSpec> l = provisionInfo.getHosts().stream() + .filter(host -> host.membership().isPresent() && matches(host.membership().get().cluster(), cluster)) + .collect(Collectors.toList()); + return l; + } + + private boolean matches(ClusterSpec nodeCluster, ClusterSpec requestedCluster) { + if (requestedCluster.group().isPresent()) // we are requesting a specific group + return nodeCluster.equals(requestedCluster); + else // we are requesting nodes of all groups in this cluster + return nodeCluster.equalsIgnoringGroup(requestedCluster); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java new file mode 100644 index 00000000000..1e7114957e9 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/StatusResource.java @@ -0,0 +1,57 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.restapi.impl; + +import com.google.common.annotations.Beta; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ModelFactory; +import com.yahoo.config.provision.Version; +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.http.v2.HttpGetConfigHandler; +import com.yahoo.vespa.config.server.http.v2.HttpListConfigsHandler; +import com.yahoo.vespa.config.server.http.v2.HttpListNamedConfigsHandler; +import com.yahoo.vespa.config.server.http.v2.SessionActiveHandler; +import com.yahoo.vespa.config.server.http.v2.SessionContentHandler; +import com.yahoo.vespa.config.server.http.v2.SessionCreateHandler; +import com.yahoo.vespa.config.server.http.v2.SessionPrepareHandler; +import com.yahoo.vespa.config.server.restapi.resources.StatusInformation; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.List; +import java.util.stream.Collectors; + +/** + * A simple status handler that can provide the status of the config server. + * + * @author lulf + * @since 5.1 + */ +@Beta +@Path("/") +@Produces(MediaType.APPLICATION_JSON) +public class StatusResource { + private final ConfigserverConfig configserverConfig; + private final List<String> modelVersions; + + @SuppressWarnings("UnusedParameters") + public StatusResource(@Component SessionCreateHandler create, + @Component SessionContentHandler content, + @Component SessionPrepareHandler prepare, + @Component SessionActiveHandler active, + @Component HttpGetConfigHandler getHandler, + @Component HttpListConfigsHandler listHandler, + @Component HttpListNamedConfigsHandler listNamedHandler, + @Component GlobalComponentRegistry componentRegistry) { + this.configserverConfig = componentRegistry.getConfigserverConfig(); + this.modelVersions = componentRegistry.getModelFactoryRegistry().getFactories().stream() + .map(ModelFactory::getVersion).map(Version::toString).collect(Collectors.toList()); + } + + @GET + public StatusInformation getStatus() { + return new StatusInformation(configserverConfig, modelVersions); + } +}
\ No newline at end of file diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java new file mode 100644 index 00000000000..ade3b6b26bc --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/impl/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.vespa.config.server.restapi.impl; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java new file mode 100644 index 00000000000..71b819edc8a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/StatusInformation.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.restapi.resources; + +import com.yahoo.config.provision.Version; +import com.yahoo.vespa.defaults.Defaults; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Status information of config server. Currently needs to convert generated configserver config to a POJO that can + * be serialized to JSON. + * + * @author lulf + * @since 5.21 + */ +public class StatusInformation { + + public ConfigserverConfig configserverConfig; + public List<String> modelVersions; + + public StatusInformation(com.yahoo.cloud.config.ConfigserverConfig configserverConfig, List<String> modelVersions) { + this.configserverConfig = new ConfigserverConfig(configserverConfig); + this.modelVersions = modelVersions; + } + + public static class ConfigserverConfig { + public final int rpcport; + public final int numthreads; + public final String zookeepercfg; + public final Collection<ZooKeeperServer> zookeeeperserver; + public final long zookeeperBarrierTimeout; + public final Collection<String> configModelPluginDir; + public final String configServerDBDir; + public final int maxgetconfigclients; + public final long sessionLifetime; + public final String applicationDirectory; + public final long masterGeneration; + public final boolean multitenant; + public final int numDelayedResponseThreads; + public final com.yahoo.cloud.config.ConfigserverConfig.PayloadCompressionType.Enum payloadCompressionType; + public final boolean useVespaVersionInRequest; + public final String serverId; + public final String region; + public final String environment; + + + public ConfigserverConfig(com.yahoo.cloud.config.ConfigserverConfig configserverConfig) { + this.rpcport = configserverConfig.rpcport(); + this.numthreads = configserverConfig.numthreads(); + this.zookeepercfg = Defaults.getDefaults().underVespaHome(configserverConfig.zookeepercfg()); + this.zookeeeperserver = configserverConfig.zookeeperserver().stream() + .map(zks -> new ZooKeeperServer(zks.hostname(), zks.port())) + .collect(Collectors.toList()); + this.zookeeperBarrierTimeout = configserverConfig.zookeeper().barrierTimeout(); + this.configModelPluginDir = configserverConfig.configModelPluginDir(); + this.configServerDBDir = Defaults.getDefaults().underVespaHome(configserverConfig.configServerDBDir()); + this.maxgetconfigclients = configserverConfig.maxgetconfigclients(); + this.sessionLifetime = configserverConfig.sessionLifetime(); + this.applicationDirectory = Defaults.getDefaults().underVespaHome(configserverConfig.applicationDirectory()); + this.masterGeneration = configserverConfig.masterGeneration(); + this.multitenant = configserverConfig.multitenant(); + this.numDelayedResponseThreads = configserverConfig.numDelayedResponseThreads(); + this.payloadCompressionType = configserverConfig.payloadCompressionType(); + this.useVespaVersionInRequest = configserverConfig.useVespaVersionInRequest(); + this.serverId = configserverConfig.serverId(); + this.region = configserverConfig.region(); + this.environment = configserverConfig.environment(); + } + } + + public static class ZooKeeperServer { + public final String hostname; + public final int port; + + public ZooKeeperServer(String hostname, int port) { + this.hostname = hostname; + this.port = port; + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java new file mode 100644 index 00000000000..1794c47a6c6 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/restapi/resources/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.vespa.config.server.restapi.resources; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java new file mode 100644 index 00000000000..9575907ab67 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/FileDistributionFactory.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.inject.Inject; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.filedistribution.FileDistributionLock; +import com.yahoo.vespa.config.server.filedistribution.FileDistributionProvider; +import com.yahoo.vespa.curator.Curator; + +import java.io.File; +import java.util.concurrent.locks.Lock; + +/** + * Factory for creating providers that are used to interact with file distribution. + * + * @author lulf + * @since 5.1 + */ +@SuppressWarnings("WeakerAccess") +public class FileDistributionFactory { + + private static final String lockPath = "/vespa/filedistribution/lock"; + private final String zkSpec; + private final Lock lock; + + @Inject + public FileDistributionFactory(Curator curator) { + this(curator, curator.connectionSpec()); + } + + public FileDistributionFactory(Curator curator, String zkSpec) { + this.lock = new FileDistributionLock(curator, lockPath); + this.zkSpec = zkSpec; + } + + public FileDistributionProvider createProvider(File applicationPackage, ApplicationId applicationId) { + return new FileDistributionProvider(applicationPackage, zkSpec, applicationId.serializedForm(), lock); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java new file mode 100644 index 00000000000..d2857068885 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSession.java @@ -0,0 +1,170 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.transaction.Transaction; +import com.yahoo.io.IOUtils; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.*; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.curator.Curator; + +import java.io.File; +import java.util.Optional; + +/** + * A LocalSession is a session that has been created locally on this configserver. A local session can be edited and + * prepared. Deleting a local session will ensure that the local filesystem state and global zookeeper state is + * cleaned for this session. + * + * @author lulf + * @since 5.1 + */ +public class LocalSession extends Session implements Comparable<LocalSession> { + + private final ApplicationPackage applicationPackage; + private final ApplicationRepo applicationRepo; + private final SessionZooKeeperClient zooKeeperClient; + private final SessionPreparer sessionPreparer; + private final SessionContext sessionContext; + private final File serverDB; + private final SuperModelGenerationCounter superModelGenerationCounter; + + /** + * Create a session. This involves loading the application, validating it and distributing it. + * + * @param sessionId The session id for this session. + */ + // TODO tenant in SessionContext? + public LocalSession(TenantName tenant, long sessionId, SessionPreparer sessionPreparer, SessionContext sessionContext) { + super(tenant, sessionId); + this.serverDB = sessionContext.getServerDBSessionDir(); + this.applicationPackage = sessionContext.getApplicationPackage(); + this.zooKeeperClient = sessionContext.getSessionZooKeeperClient(); + this.applicationRepo = sessionContext.getApplicationRepo(); + this.sessionPreparer = sessionPreparer; + this.sessionContext = sessionContext; + this.superModelGenerationCounter = sessionContext.getSuperModelGenerationCounter(); + } + + public ConfigChangeActions prepare(DeployLogger logger, PrepareParams params, Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) { + Curator.CompletionWaiter waiter = zooKeeperClient.createPrepareWaiter(); + ConfigChangeActions actions = sessionPreparer.prepare(sessionContext, logger, params, currentActiveApplicationSet, tenantPath); + setPrepared(); + waiter.awaitCompletion(params.getTimeoutBudget().timeLeft()); + return actions; + } + + public ApplicationFile getApplicationFile(Path relativePath, Mode mode) { + if (mode.equals(Mode.WRITE)) { + markSessionEdited(); + } + return applicationPackage.getFile(relativePath); + } + + private void setPrepared() { + setStatus(Session.Status.PREPARE); + } + + private Transaction setActive() { + Transaction transaction = createSetStatusTransaction(Status.ACTIVATE); + transaction.add(applicationRepo.createPutApplicationTransaction(zooKeeperClient.readApplicationId(getTenant()), getSessionId()).operations()); + return transaction; + } + + private Transaction createSetStatusTransaction(Status status) { + return zooKeeperClient.createWriteStatusTransaction(status); + } + + public Session.Status getStatus() { + return zooKeeperClient.readStatus(); + } + + private void setStatus(Session.Status newStatus) { + zooKeeperClient.writeStatus(newStatus); + } + + public Transaction createActivateTransaction() { + zooKeeperClient.createActiveWaiter(); + superModelGenerationCounter.increment(); + return setActive(); + } + + public Transaction createDeactivateTransaction() { + return createSetStatusTransaction(Status.DEACTIVATE); + } + + private void markSessionEdited() { + setStatus(Session.Status.NEW); + } + + public long getActiveSessionAtCreate() { + return applicationPackage.getMetaData().getPreviousActiveGeneration(); + } + + // Note: Assumes monotonically increasing session ids + public boolean isNewerThan(long sessionId) { + return getSessionId() > sessionId; + } + + /** + * Deletes this session from ZooKeeper and filesystem, as well as making sure the supermodel generation counter is incremented. + */ + public void delete() { + superModelGenerationCounter.increment(); + IOUtils.recursiveDeleteDir(serverDB); + zooKeeperClient.delete(); + } + + @Override + public int compareTo(LocalSession rhs) { + Long lhsId = getSessionId(); + Long rhsId = rhs.getSessionId(); + return lhsId.compareTo(rhsId); + } + + // in seconds + public long getCreateTime() { + return zooKeeperClient.readCreateTime(); + } + + public void waitUntilActivated(TimeoutBudget timeoutBudget) { + zooKeeperClient.getActiveWaiter().awaitCompletion(timeoutBudget.timeLeft()); + } + + public void setApplicationId(ApplicationId applicationId) { + zooKeeperClient.writeApplicationId(applicationId); + } + + public enum Mode { + READ, WRITE + } + + public ApplicationMetaData getMetaData() { + return applicationPackage.getMetaData(); + } + + public ApplicationId getApplicationId() { + return zooKeeperClient.readApplicationId(getTenant()); + } + + public ProvisionInfo getProvisionInfo() { + return zooKeeperClient.getProvisionInfo(); + } + + @Override + public String logPre() { + if (getApplicationId().equals(ApplicationId.defaultId())) { + return Tenants.logPre(getTenant()); + } else { + return Tenants.logPre(getApplicationId()); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java new file mode 100644 index 00000000000..3aa8155a7c6 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionLoader.java @@ -0,0 +1,14 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +/** + * Interface of a component that is able to load a session given a session id. + * + * @author lulf + * @since 5.1 + */ +public interface LocalSessionLoader { + + LocalSession loadSession(long sessionId); + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java new file mode 100644 index 00000000000..8f39d0b96d1 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/LocalSessionRepo.java @@ -0,0 +1,121 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.log.LogLevel; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; + +import java.io.File; +import java.io.FilenameFilter; +import java.time.Clock; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * File-based session repository for LocalSessions. Contains state for the local instance of the configserver. + * + * @author lulf + * @since 5.1 + */ +public class LocalSessionRepo extends SessionRepo<LocalSession> { + + private static final Logger log = Logger.getLogger(LocalSessionRepo.class.getName()); + + private final static FilenameFilter sessionApplicationsFilter = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.matches("\\d+"); + } + }; + + private final long sessionLifetime; // in seconds + private final ApplicationRepo applicationRepo; + private final Clock clock; + + public LocalSessionRepo(TenantFileSystemDirs tenantFileSystemDirs, LocalSessionLoader loader, ApplicationRepo applicationRepo, Clock clock, long sessionLifeTime) { + this(applicationRepo, clock, sessionLifeTime); + loadSessions(tenantFileSystemDirs.path(), loader); + } + + private void loadSessions(File applicationsDir, LocalSessionLoader loader) { + File[] applications = applicationsDir.listFiles(sessionApplicationsFilter); + if (applications == null) { + return; + } + for (File application : applications) { + try { + addSession(loader.loadSession(Long.parseLong(application.getName()))); + } catch (IllegalArgumentException e) { + log.log(LogLevel.WARNING, "Could not load application '" + application.getAbsolutePath() + "':" + e.getMessage() + ", skipping it."); + } + } + } + + /** + * Gets the active Session for the given application id. + * + * @return the active session, or null if there is no active session for the given application id. + */ + public LocalSession getActiveSession(ApplicationId applicationId) { + List<ApplicationId> applicationIds = applicationRepo.listApplications(); + if (applicationIds.contains(applicationId)) { + return getSession(applicationRepo.getSessionIdForApplication(applicationId)); + } + return null; + } + + // Constructor only for testing + public LocalSessionRepo(ApplicationRepo applicationRepo, Clock clock, long sessionLifetime) { + this.applicationRepo = applicationRepo; + this.sessionLifetime = sessionLifetime; + this.clock = clock; + } + + public LocalSessionRepo(ApplicationRepo applicationRepo) { + this(applicationRepo, Clock.systemUTC(), TimeUnit.DAYS.toMillis(1)); + } + + @Override + public synchronized void addSession(LocalSession session) { + purgeOldSessions(); + super.addSession(session); + } + + private void purgeOldSessions() { + final List<ApplicationId> applicationIds = applicationRepo.listApplications(); + List<LocalSession> sessions = new ArrayList<>(listSessions()); + for (LocalSession candidate : sessions) { + if (hasExpired(candidate) && !isActiveSession(candidate, applicationIds)) { + deleteSession(candidate); + } + } + } + + private boolean hasExpired(LocalSession candidate) { + return (candidate.getCreateTime() + sessionLifetime) <= TimeUnit.MILLISECONDS.toSeconds(clock.millis()); + } + + private boolean isActiveSession(LocalSession candidate, List<ApplicationId> activeIds) { + if (candidate.getStatus() == Session.Status.ACTIVATE && activeIds.contains(candidate.getApplicationId())) { + long sessionId = applicationRepo.getSessionIdForApplication(candidate.getApplicationId()); + return (candidate.getSessionId() == sessionId); + } else { + return false; + } + } + + private void deleteSession(LocalSession candidate) { + removeSession(candidate.getSessionId()); + candidate.delete(); + } + + public void deleteAllSessions() { + List<LocalSession> sessions = new ArrayList<>(listSessions()); + for (LocalSession session : sessions) { + deleteSession(session); + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java new file mode 100644 index 00000000000..6c9224db0fe --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/PrepareParams.java @@ -0,0 +1,165 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Rotation; +import com.yahoo.config.provision.TenantName; +import com.yahoo.config.provision.Version; +import com.yahoo.container.jdisc.HttpRequest; +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.http.SessionHandler; + +import java.time.Clock; +import java.time.Duration; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +/** + * Parameters for prepare. + * + * @author lulf + * @since 5.1.24 + */ +public final class PrepareParams { + + static final String APPLICATION_NAME_PARAM_NAME = "applicationName"; + static final String INSTANCE_PARAM_NAME = "instance"; + static final String IGNORE_VALIDATION_PARAM_NAME = "ignoreValidationErrors"; + static final String DRY_RUN_PARAM_NAME = "dryRun"; + static final String VESPA_VERSION_PARAM_NAME = "vespaVersion"; + static final String ROTATIONS_PARAM_NAME = "rotations"; + static final String DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME = "dockerVespaImageVersion"; + + private boolean ignoreValidationErrors = false; + private boolean dryRun = false; + private ApplicationId applicationId = ApplicationId.defaultId(); + private TimeoutBudget timeoutBudget; + private Optional<Version> vespaVersion = Optional.empty(); + private Set<Rotation> rotations; + private Optional<Version> dockerVespaImageVersion = Optional.empty(); + + PrepareParams() { + this(new ConfigserverConfig(new ConfigserverConfig.Builder())); + } + + public PrepareParams(ConfigserverConfig configserverConfig) { + timeoutBudget = new TimeoutBudget(Clock.systemUTC(), getBarrierTimeout(configserverConfig)); + } + + public PrepareParams applicationId(ApplicationId applicationId) { + this.applicationId = applicationId; + return this; + } + + public PrepareParams ignoreValidationErrors(boolean ignoreValidationErrors) { + this.ignoreValidationErrors = ignoreValidationErrors; + return this; + } + + public PrepareParams dryRun(boolean dryRun) { + this.dryRun = dryRun; + return this; + } + + public PrepareParams timeoutBudget(TimeoutBudget timeoutBudget) { + this.timeoutBudget = timeoutBudget; + return this; + } + + public PrepareParams vespaVersion(String vespaVersion) { + Optional<Version> version = Optional.empty(); + if (vespaVersion != null && !vespaVersion.isEmpty()) { + version = Optional.of(Version.fromString(vespaVersion)); + } + this.vespaVersion = version; + return this; + } + + public PrepareParams rotations(String rotationsString) { + this.rotations = new LinkedHashSet<>(); + if (rotationsString != null && !rotationsString.isEmpty()) { + String[] rotations = rotationsString.split(","); + for (String s : rotations) { + this.rotations.add(new Rotation(s)); + } + } + return this; + } + + public PrepareParams dockerVespaImageVersion(String dockerVespaImageVersion) { + Optional<Version> version = Optional.empty(); + if (dockerVespaImageVersion != null && !dockerVespaImageVersion.isEmpty()) { + version = Optional.of(Version.fromString(dockerVespaImageVersion)); + } + this.dockerVespaImageVersion = version; + return this; + } + + public static PrepareParams fromHttpRequest(HttpRequest request, TenantName tenant, ConfigserverConfig configserverConfig) { + return new PrepareParams(configserverConfig).ignoreValidationErrors(request.getBooleanProperty(IGNORE_VALIDATION_PARAM_NAME)) + .dryRun(request.getBooleanProperty(DRY_RUN_PARAM_NAME)) + .timeoutBudget(SessionHandler.getTimeoutBudget(request, getBarrierTimeout(configserverConfig))) + .applicationId(createApplicationId(request, tenant)) + .vespaVersion(request.getProperty(VESPA_VERSION_PARAM_NAME)) + .rotations(request.getProperty(ROTATIONS_PARAM_NAME)) + .dockerVespaImageVersion(request.getProperty(DOCKER_VESPA_IMAGE_VERSION_PARAM_NAME)); + } + + private static Duration getBarrierTimeout(ConfigserverConfig configserverConfig) { + return Duration.ofSeconds(configserverConfig.zookeeper().barrierTimeout()); + } + + private static ApplicationId createApplicationId(HttpRequest request, TenantName tenant) { + return new ApplicationId.Builder() + .tenant(tenant) + .applicationName(getPropertyWithDefault(request, APPLICATION_NAME_PARAM_NAME, "default")) + .instanceName(getPropertyWithDefault(request, INSTANCE_PARAM_NAME, "default")) + .build(); + } + + private static String getPropertyWithDefault(HttpRequest request, String propertyName, String defaultProperty) { + return getProperty(request, propertyName).orElse(defaultProperty); + } + + private static Optional<String> getProperty(HttpRequest request, String propertyName) { + return Optional.ofNullable(request.getProperty(propertyName)); + } + + public String getApplicationName() { + return applicationId.application().value(); + } + + public ApplicationId getApplicationId() { + return applicationId; + } + + public Optional<Version> vespaVersion() { return vespaVersion; } + + public Set<Rotation> rotations() { return rotations; } + + public boolean ignoreValidationErrors() { + return ignoreValidationErrors; + } + + public boolean isDryRun() { + return dryRun; + } + + public TimeoutBudget getTimeoutBudget() { + return timeoutBudget; + } + + public Optional<Version> getVespaVersion() { + return vespaVersion; + } + + public Set<Rotation> getRotations() { + return rotations; + } + + public Optional<Version> getDockerVespaImageVersion() { + return dockerVespaImageVersion; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java new file mode 100644 index 00000000000..2ce378d0464 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSession.java @@ -0,0 +1,97 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.*; +import com.yahoo.vespa.config.server.*; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.server.modelfactory.ActivatedModelsBuilder; +import com.yahoo.vespa.curator.Curator; + +import java.util.*; +import java.util.logging.Logger; + +/** + * A RemoteSession represents a session created on another config server. This session can + * be regarded as read only, and this interface only allows reading information about a session. + * + * @author lulf + * @since 5.1 + */ +public class RemoteSession extends Session { + + private static final Logger log = Logger.getLogger(RemoteSession.class.getName()); + private volatile ApplicationSet applicationSet = null; + private final SessionZooKeeperClient zooKeeperClient; + private final ActivatedModelsBuilder applicationLoader; + + /** + * Creates a session. This involves loading the application, validating it and distributing it. + * + * @param tenant The name of the tenant creating session + * @param sessionId The session id for this session. + * @param globalComponentRegistry a registry of global components + * @param zooKeeperClient a SessionZooKeeperClient instance + */ + public RemoteSession(TenantName tenant, + long sessionId, + GlobalComponentRegistry globalComponentRegistry, + SessionZooKeeperClient zooKeeperClient) { + super(tenant, sessionId); + this.zooKeeperClient = zooKeeperClient; + this.applicationLoader = new ActivatedModelsBuilder(tenant, sessionId, zooKeeperClient, globalComponentRegistry); + } + + public void loadPrepared() { + Curator.CompletionWaiter waiter = zooKeeperClient.getPrepareWaiter(); + ensureApplicationLoaded(); + waiter.notifyCompletion(); + } + + private ApplicationSet loadApplication() { + return ApplicationSet.fromList(applicationLoader.buildModels(zooKeeperClient.readApplicationId(getTenant()), + zooKeeperClient.loadApplicationPackage())); + } + + public ApplicationSet ensureApplicationLoaded() { + if (applicationSet == null) { + applicationSet = loadApplication(); + } + return applicationSet; + } + + public Session.Status getStatus() { + return zooKeeperClient.readStatus(); + } + + public void deactivate() { + applicationSet = null; + } + + public void makeActive(ReloadHandler reloadHandler) { + Curator.CompletionWaiter waiter = zooKeeperClient.getActiveWaiter(); + log.log(LogLevel.DEBUG, logPre()+"Getting session from repo: " + getSessionId()); + ApplicationSet app = ensureApplicationLoaded(); + log.log(LogLevel.DEBUG, logPre() + "Reloading config for " + app); + reloadHandler.reloadConfig(app); + log.log(LogLevel.DEBUG, logPre() + "Notifying " + waiter); + waiter.notifyCompletion(); + log.log(LogLevel.DEBUG, logPre() + "Session activated: " + app); + } + + @Override + public String logPre() { + if (applicationSet != null) { + return Tenants.logPre(applicationSet.getForVersionOrLatest(Optional.empty()).getId()); + } + + return Tenants.logPre(getTenant()); + } + + public void confirmUpload() { + Curator.CompletionWaiter waiter = zooKeeperClient.getUploadWaiter(); + log.log(LogLevel.DEBUG, "Notifying upload waiter for session " + getSessionId()); + waiter.notifyCompletion(); + log.log(LogLevel.DEBUG, "Done notifying for session " + getSessionId()); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java new file mode 100644 index 00000000000..c44436740be --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionFactory.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.path.Path; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.vespa.config.server.GlobalComponentRegistry; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; + +/** + * @author lulf + * @since 5.1.24 + */ +public class RemoteSessionFactory { + + private final GlobalComponentRegistry componentRegistry; + private final Curator curator; + private final ConfigCurator configCurator; + private final Path sessionDirPath; + private final ConfigDefinitionRepo defRepo; + private final TenantName tenant; + private final ConfigserverConfig configserverConfig; + + public RemoteSessionFactory(GlobalComponentRegistry componentRegistry, + Path sessionsPath, + TenantName tenant) { + this.componentRegistry = componentRegistry; + this.curator = componentRegistry.getCurator(); + this.configCurator = componentRegistry.getConfigCurator(); + this.sessionDirPath = sessionsPath; + this.tenant = tenant; + this.defRepo = componentRegistry.getConfigDefinitionRepo(); + this.configserverConfig = componentRegistry.getConfigserverConfig(); + } + + public RemoteSession createSession(long sessionId) { + Path sessionPath = sessionDirPath.append(String.valueOf(sessionId)); + SessionZooKeeperClient sessionZKClient = new SessionZooKeeperClient(curator, configCurator, sessionPath, defRepo, configserverConfig.serverId()); + return new RemoteSession(tenant, sessionId, componentRegistry, sessionZKClient); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java new file mode 100644 index 00000000000..78d7704506f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/RemoteSessionRepo.java @@ -0,0 +1,226 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Multiset; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.yolean.Exceptions; +import com.yahoo.vespa.config.server.ReloadHandler; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.cache.*; + +/** + * Will watch/prepare sessions (applications) based on watched nodes in ZooKeeper, set for example + * by the prepare HTTP handler on another configserver. The zookeeper state watched in this class is shared + * between all configservers, so it should not modify any global state, because the operation will be performed + * on all servers. The repo can be regarded as read only from the POV of the configserver. + * + * @author vegardh + * @author lulf + * @since 5.1 + */ +public class RemoteSessionRepo extends SessionRepo<RemoteSession> implements NodeCacheListener, PathChildrenCacheListener { + + private static final Logger log = Logger.getLogger(RemoteSessionRepo.class.getName()); + private final Curator curator; + private final Path sessionsPath; + private final RemoteSessionFactory remoteSessionFactory; + private final Map<Long, SessionStateWatcher> sessionStateWatchers = new HashMap<>(); + private final ReloadHandler reloadHandler; + private final MetricUpdater metrics; + private final Curator.DirectoryCache directoryCache; + private final ApplicationRepo applicationRepo; + + public static RemoteSessionRepo create(Curator curator, + RemoteSessionFactory remoteSessionFactory, + ReloadHandler reloadHandler, + Path sessionsPath, + ApplicationRepo applicationRepo, + MetricUpdater metrics, + ExecutorService executorService) throws Exception { + return new RemoteSessionRepo(curator, remoteSessionFactory, reloadHandler, sessionsPath, applicationRepo, metrics, executorService); + } + + /** + * Used when the RemoteSessionRepo is set up programmatically from a Tenant, i.e. config v2 + * @param curator a {@link Curator} instance. + * @param remoteSessionFactory a {@link com.yahoo.vespa.config.server.session.RemoteSessionFactory} + * @param reloadHandler a {@link com.yahoo.vespa.config.server.ReloadHandler} + * @param sessionsPath a {@link com.yahoo.path.Path} to the sessions dir. + * @param applicationRepo an {@link com.yahoo.vespa.config.server.application.ApplicationRepo} object. + * @param executorService an {@link ExecutorService} to run callbacks from ZooKeeper. + * @throws java.lang.Exception if creating the repo fails + */ + private RemoteSessionRepo(Curator curator, + RemoteSessionFactory remoteSessionFactory, + ReloadHandler reloadHandler, + Path sessionsPath, + ApplicationRepo applicationRepo, + MetricUpdater metricUpdater, + ExecutorService executorService) throws Exception { + this.curator = curator; + this.sessionsPath = sessionsPath; + this.applicationRepo = applicationRepo; + this.remoteSessionFactory = remoteSessionFactory; + this.reloadHandler = reloadHandler; + this.metrics = metricUpdater; + this.directoryCache = curator.createDirectoryCache(sessionsPath.getAbsolute(), false, false, executorService); + this.directoryCache.start(); + this.directoryCache.addListener(this); + sessionsChanged(getSessionList(directoryCache.getCurrentData())); + } + + private void loadActiveSession(RemoteSession session) { + tryReload(session.ensureApplicationLoaded(), session.logPre()); + } + + private void tryReload(ApplicationSet applicationSet, String logPre) { + try { + reloadHandler.reloadConfig(applicationSet); + log.log(LogLevel.INFO, logPre+"Application activated successfully: " + applicationSet.getId()); + } catch (Exception e) { + log.log(LogLevel.WARNING, logPre+"Skipping loading of application '" + applicationSet.getId() + "': " + Exceptions.toMessageString(e)); + } + } + + // For testing only + public RemoteSessionRepo() { + this.curator = null; + this.remoteSessionFactory = null; + this.reloadHandler = null; + this.sessionsPath = Path.createRoot(); + this.metrics = null; + this.directoryCache = null; + this.applicationRepo = null; + } + + private List<Long> getSessionList(List<ChildData> children) { + List<Long> sessions = new ArrayList<>(); + for (ChildData data : children) { + sessions.add(Long.parseLong(Path.fromString(data.getPath()).getName())); + } + return sessions; + } + + synchronized void sessionsChanged(List<Long> sessions) throws NumberFormatException { + checkForRemovedSessions(sessions); + checkForAddedSessions(sessions); + } + + private void checkForRemovedSessions(List<Long> sessions) { + for (RemoteSession session : listSessions()) { + if (!sessions.contains(session.getSessionId())) { + SessionStateWatcher watcher = sessionStateWatchers.remove(session.getSessionId()); + watcher.close(); + removeSession(session.getSessionId()); + metrics.incRemovedSessions(); + } + } + } + + private void checkForAddedSessions(List<Long> sessions) { + for (Long sessionId : sessions) { + if (getSession(sessionId) == null) { + log.log(LogLevel.DEBUG, "Loading session id " + sessionId); + newSession(sessionId); + metrics.incAddedSessions(); + } + } + } + + /** + * A session for which we don't have a watcher, i.e. hitherto unknown to us. + * + * @param sessionId session id for the new session + */ + private void newSession(long sessionId) { + try { + log.log(LogLevel.DEBUG, "Adding session to RemoteSessionRepo: " + sessionId); + RemoteSession session = remoteSessionFactory.createSession(sessionId); + Path sessionPath = sessionsPath.append(String.valueOf(sessionId)); + Curator.FileCache fileCache = curator.createFileCache(sessionPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH).getAbsolute(), false); + fileCache.addListener(this); + loadSessionIfActive(session); + sessionStateWatchers.put(sessionId, new SessionStateWatcher(fileCache, reloadHandler, session, metrics)); + addSession(session); + } catch (Exception e) { + log.log(Level.WARNING, "Failed loading session " + sessionId + " (no config for this session can be served) : " + Exceptions.toMessageString(e)); + } + } + + private void loadSessionIfActive(RemoteSession session) { + for (ApplicationId applicationId : applicationRepo.listApplications()) { + try { + if (applicationRepo.getSessionIdForApplication(applicationId) == session.getSessionId()) { + log.log(LogLevel.DEBUG, "Found active application for session " + session.getSessionId() + " , loading it"); + loadActiveSession(session); + break; + } + } catch (Exception e) { + log.log(LogLevel.WARNING, session.logPre() + " error reading session id for " + applicationId); + } + } + } + + public synchronized void close() { + try { + if (directoryCache != null) { + directoryCache.close(); + } + } catch (Exception e) { + log.log(LogLevel.WARNING, "Exception when closing path cache", e); + } finally { + checkForRemovedSessions(new ArrayList<>()); + } + } + + @Override + public void nodeChanged() throws Exception { + Multiset<Session.Status> sessionMetrics = HashMultiset.create(); + for (RemoteSession session : listSessions()) { + sessionMetrics.add(session.getStatus()); + } + metrics.setNewSessions(sessionMetrics.count(Session.Status.NEW)); + metrics.setPreparedSessions(sessionMetrics.count(Session.Status.PREPARE)); + metrics.setActivatedSessions(sessionMetrics.count(Session.Status.ACTIVATE)); + metrics.setDeactivatedSessions(sessionMetrics.count(Session.Status.DEACTIVATE)); + } + + @Override + public void childEvent(CuratorFramework framework, PathChildrenCacheEvent event) throws Exception { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Got child event: " + event); + } + switch (event.getType()) { + case CHILD_ADDED: + sessionsChanged(getSessionList(directoryCache.getCurrentData())); + synchronizeOnNew(getSessionList(Collections.singletonList(event.getData()))); + break; + case CHILD_REMOVED: + sessionsChanged(getSessionList(directoryCache.getCurrentData())); + break; + } + } + + private void synchronizeOnNew(List<Long> sessionList) { + for (long sessionId : sessionList) { + RemoteSession session = getSession(sessionId); + log.log(LogLevel.DEBUG, session.logPre() + "Confirming upload for session " + sessionId); + session.confirmUpload(); + + } + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java new file mode 100644 index 00000000000..329eab89458 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/ServerCacheLoader.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.base.Splitter; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.subscription.CfgConfigPayloadBuilder; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.vespa.config.util.ConfigUtils; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +import java.util.Arrays; +import java.util.Map; + +/** + * This class is tasked with reading config definitions and legacy configs/ from zookeeper, and create + * a {@link ServerCache} instance containing these in memory. + * + * @author lulf + * @since 5.1 + */ +public class ServerCacheLoader { + + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ServerCacheLoader.class.getName()); + private final ConfigDefinitionRepo repo; + private final ConfigCurator configCurator; + private final Path path; + public ServerCacheLoader(ConfigCurator configCurator, Path rootPath, ConfigDefinitionRepo repo) { + this.configCurator = configCurator; + this.path = rootPath; + this.repo = repo; + } + + public ServerCache loadCache() { + return loadConfigDefinitions(); + } + + /** + * Reads config definitions from zookeeper, parses them and puts both ConfigDefinition instances + * and payload (raw config definition) into cache. + * + * @return the populated cache. + */ + public ServerCache loadConfigDefinitions() { + ServerCache cache = new ServerCache(); + try { + log.log(LogLevel.DEBUG, "Getting config definitions"); + loadGlobalConfigDefinitions(cache); + loadConfigDefinitionsFromPath(cache, path.append(ConfigCurator.USER_DEFCONFIGS_ZK_SUBPATH).getAbsolute()); + log.log(LogLevel.DEBUG, "Done getting config definitions"); + } catch (Exception e) { + throw new IllegalStateException("Could not load config definitions for " + path, e); + } + return cache; + } + + private void loadGlobalConfigDefinitions(ServerCache cache) { + for (Map.Entry<ConfigDefinitionKey, ConfigDefinition> entry : repo.getConfigDefinitions().entrySet()) { + cache.addDef(entry.getKey(), entry.getValue()); + } + } + + /** + * Loads config definitions from a specified path into server cache and returns it. + * + * @param appPath the path to load config definitions from + */ + private void loadConfigDefinitionsFromPath(ServerCache cache, String appPath) throws InterruptedException { + if ( ! configCurator.exists(appPath)) return; + for (String nodeName : configCurator.getChildren(appPath)) { + String payload = configCurator.getData(appPath, nodeName); + ConfigDefinitionKey dKey = ConfigUtils.createConfigDefinitionKeyFromZKString(nodeName); + cache.addDef(dKey, new ConfigDefinition(dKey.getName(), Splitter.on("\n").splitToList(payload).toArray(new String[0]))); + } + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java new file mode 100644 index 00000000000..961f9d10a60 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/Session.java @@ -0,0 +1,65 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.Tenants; + +/** + * A session represents an instance of an application that can be edited, prepared and activated. This + * class represents the common stuff between sessions working on the local file + * system ({@link LocalSession}s) and sessions working on zookeeper {@link RemoteSession}s. + * + * @author lulf + * @since 5.1 + */ +public abstract class Session { + + private final long sessionId; + protected final TenantName tenant; + + protected Session(TenantName tenant, long sessionId) { + this.tenant = tenant; + this.sessionId = sessionId; + } + /** + * Retrieve the session id for this session. + * @return the session id. + */ + public final long getSessionId() { + return sessionId; + } + + @Override + public String toString() { + return "Session,id=" + sessionId; + } + + /** + * Represents the status of this session. + */ + public enum Status { + NEW, PREPARE, ACTIVATE, DEACTIVATE, NONE; + + public static Status parse(String data) { + for (Status status : Status.values()) { + if (status.name().equals(data)) { + return status; + } + } + return Status.NEW; + } + } + + public TenantName getTenant() { + return tenant; + } + + /** + * Helper to provide a log message preamble for code dealing with sessions + * @return log preamble + */ + public String logPre() { + return Tenants.logPre(getTenant()); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java new file mode 100644 index 00000000000..dd908eaa559 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionContext.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.vespa.config.server.HostValidator; +import com.yahoo.vespa.config.server.SuperModelGenerationCounter; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.application.ApplicationRepo; + +import java.io.File; + +/** + * The dependencies needed for a local session to be edited and prepared. + * + * @author lulf + * @since 5.1 + */ +public class SessionContext { + + private final ApplicationPackage applicationPackage; + private final SessionZooKeeperClient sessionZooKeeperClient; + private final File serverDBSessionDir; + private final ApplicationRepo applicationRepo; + private final HostValidator<ApplicationId> hostRegistry; + private final SuperModelGenerationCounter superModelGenerationCounter; + + public SessionContext(ApplicationPackage applicationPackage, SessionZooKeeperClient sessionZooKeeperClient, + File serverDBSessionDir, ApplicationRepo applicationRepo, + HostValidator<ApplicationId> hostRegistry, SuperModelGenerationCounter superModelGenerationCounter) { + this.applicationPackage = applicationPackage; + this.sessionZooKeeperClient = sessionZooKeeperClient; + this.serverDBSessionDir = serverDBSessionDir; + this.applicationRepo = applicationRepo; + this.hostRegistry = hostRegistry; + this.superModelGenerationCounter = superModelGenerationCounter; + } + + public ApplicationPackage getApplicationPackage() { + return applicationPackage; + } + + public SessionZooKeeperClient getSessionZooKeeperClient() { + return sessionZooKeeperClient; + } + + public File getServerDBSessionDir() { + return serverDBSessionDir; + } + + public ApplicationRepo getApplicationRepo() { + return applicationRepo; + } + + public HostValidator<ApplicationId> getHostValidator() { return hostRegistry; } + + public SuperModelGenerationCounter getSuperModelGenerationCounter() { + return superModelGenerationCounter; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java new file mode 100644 index 00000000000..87af5351186 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactory.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.vespa.config.server.TimeoutBudget; + +import java.io.File; + +/** + * A session factory responsible for creating deploy sessions. + * + * @author lulf + * @since 5.1 + */ +public interface SessionFactory { + /** + * Creates a new deployment session from an application package. + * + * + * + * @param applicationDirectory a File pointing to an application. + * @param applicationName name of the application for this new session. + * @param logger a deploy logger where the deploy log will be written. + * @param timeoutBudget Timeout for creating session and waiting for other servers. + * @return a new session + */ + LocalSession createSession(File applicationDirectory, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget); + + /** + * Creates a new deployment session from an already existing session. + * + * @param existingSession The session to use as base + * @param logger a deploy logger where the deploy log will be written. + * @param timeoutBudget Timeout for creating session and waiting for other servers. + * @return a new session + */ + LocalSession createSessionFromExisting(LocalSession existingSession, DeployLogger logger, TimeoutBudget timeoutBudget); +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java new file mode 100644 index 00000000000..3ef6f23b84e --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionFactoryImpl.java @@ -0,0 +1,170 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.model.application.provider.*; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.io.IOUtils; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.*; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.TenantName; +import com.yahoo.vespa.config.server.application.ApplicationRepo; +import com.yahoo.vespa.config.server.deploy.TenantFileSystemDirs; +import com.yahoo.vespa.config.server.zookeeper.SessionCounter; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; +import com.yahoo.vespa.curator.Curator; + +import java.io.File; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Serves as the factory of sessions. Takes care of copying files to the correct folder and initializing the + * session state. + * + * @author lulf + * @since 5.1 + */ +public class SessionFactoryImpl implements SessionFactory, LocalSessionLoader { + + private static final Logger log = Logger.getLogger(SessionFactoryImpl.class.getName()); + private static final long nonExistingActiveSession = 0; + + private final SessionPreparer sessionPreparer; + private final Curator curator; + private final ConfigCurator configCurator; + private final SessionCounter sessionCounter; + private final ApplicationRepo applicationRepo; + private final Path sessionsPath; + private final TenantFileSystemDirs tenantFileSystemDirs; + private final HostValidator<ApplicationId> hostRegistry; + private final SuperModelGenerationCounter superModelGenerationCounter; + private final ConfigDefinitionRepo defRepo; + private final TenantName tenant; + private final String serverId; + + public SessionFactoryImpl(GlobalComponentRegistry globalComponentRegistry, + SessionCounter sessionCounter, + Path sessionsPath, + ApplicationRepo applicationRepo, + TenantFileSystemDirs tenantFileSystemDirs, HostValidator<ApplicationId> hostRegistry, TenantName tenant) { + this.hostRegistry = hostRegistry; + this.tenant = tenant; + this.sessionPreparer = globalComponentRegistry.getSessionPreparer(); + this.curator = globalComponentRegistry.getCurator(); + this.configCurator = globalComponentRegistry.getConfigCurator(); + this.sessionCounter = sessionCounter; + this.sessionsPath = sessionsPath; + this.applicationRepo = applicationRepo; + this.tenantFileSystemDirs = tenantFileSystemDirs; + this.superModelGenerationCounter = globalComponentRegistry.getSuperModelGenerationCounter(); + this.defRepo = globalComponentRegistry.getConfigDefinitionRepo(); + this.serverId = globalComponentRegistry.getConfigserverConfig().serverId(); + } + + @Override + public LocalSession createSession(File applicationFile, String applicationName, DeployLogger logger, TimeoutBudget timeoutBudget) { + return create(applicationFile, applicationName, logger, nonExistingActiveSession, timeoutBudget); + } + + private void ensureZKPathDoesNotExist(Path sessionPath) { + if (configCurator.exists(sessionPath.getAbsolute())) { + throw new IllegalArgumentException("Path " + sessionPath.getAbsolute() + " already exists in ZooKeeper"); + } + } + + private ApplicationPackage createApplication(File userDir, + File configApplicationDir, + String applicationName, + long sessionId, + long currentlyActiveSession) { + long deployTimestamp = System.currentTimeMillis(); + String user = System.getenv("USER"); + if (user == null) { + user = "unknown"; + } + DeployData deployData = new DeployData(user, userDir.getAbsolutePath(), applicationName, deployTimestamp, sessionId, currentlyActiveSession); + return FilesApplicationPackage.fromFileWithDeployData(configApplicationDir, deployData); + } + + private LocalSession createSessionFromApplication(ApplicationPackage applicationPackage, + long sessionId, + SessionZooKeeperClient sessionZKClient, TimeoutBudget timeoutBudget) throws Exception { + log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Creating session " + sessionId + " in ZooKeeper"); + sessionZKClient.createNewSession(System.currentTimeMillis(), TimeUnit.MILLISECONDS); + log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Creating upload waiter for session " + sessionId); + Curator.CompletionWaiter waiter = sessionZKClient.getUploadWaiter(); + log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Done creating upload waiter for session " + sessionId); + LocalSession session = new LocalSession(tenant, sessionId, sessionPreparer, new SessionContext(applicationPackage, sessionZKClient, getSessionAppDir(sessionId), applicationRepo, hostRegistry, superModelGenerationCounter)); + log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Waiting on upload waiter for session " + sessionId); + waiter.awaitCompletion(timeoutBudget.timeLeft()); + log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Done waiting on upload waiter for session " + sessionId); + return session; + } + + @Override + public LocalSession createSessionFromExisting(LocalSession existingSession, + DeployLogger logger, + TimeoutBudget timeoutBudget) { + File existingApp = getSessionAppDir(existingSession.getSessionId()); + ApplicationMetaData metaData = FilesApplicationPackage.readMetaData(existingApp); + final ApplicationId existingSessionId = existingSession.getApplicationId(); + + + final long liveApp = getLiveApp(existingSessionId); + logger.log(LogLevel.DEBUG, "Create from existing application id " + existingSessionId + ", live app for it is " + liveApp); + LocalSession session = create(existingApp, metaData.getApplicationName(), logger, liveApp, timeoutBudget); + session.setApplicationId(existingSessionId); + return session; + } + + private LocalSession create(File applicationFile, String applicationName, DeployLogger logger, long currentlyActiveSession, TimeoutBudget timeoutBudget) { + long sessionId = sessionCounter.nextSessionId(); + Path sessionIdPath = sessionsPath.append(String.valueOf(sessionId)); + log.log(LogLevel.DEBUG, Tenants.logPre(tenant) + "Next session id is " + sessionId + " , sessionIdPath=" + sessionIdPath.getAbsolute()); + try { + ensureZKPathDoesNotExist(sessionIdPath); + SessionZooKeeperClient sessionZooKeeperClient = new SessionZooKeeperClient(curator, configCurator, sessionIdPath, defRepo, serverId); + File userApplicationDir = tenantFileSystemDirs.getUserApplicationDir(sessionId); + IOUtils.copyDirectory(applicationFile, userApplicationDir); + ApplicationPackage applicationPackage = createApplication(applicationFile, userApplicationDir, applicationName, sessionId, currentlyActiveSession); + applicationPackage.writeMetaData(); + logger.log(LogLevel.SPAM, "Application package is written to disk"); + return createSessionFromApplication(applicationPackage, sessionId, sessionZooKeeperClient, timeoutBudget); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Error creating session: " + e.getMessage(), e); + } + } + + private File getSessionAppDir(long sessionId) { + File appDir = tenantFileSystemDirs.getUserApplicationDir(sessionId); + if (!appDir.exists() || !appDir.isDirectory()) { + throw new IllegalArgumentException("Unable to find correct application directory for session " + sessionId); + } + return appDir; + } + + @Override + public LocalSession loadSession(long sessionId) { + File sessionDir = getSessionAppDir(sessionId); + ApplicationPackage applicationPackage = FilesApplicationPackage.fromFile(sessionDir); + Path sessionIdPath = sessionsPath.append(String.valueOf(sessionId)); + SessionZooKeeperClient sessionZKClient = new SessionZooKeeperClient(curator, configCurator, sessionIdPath, defRepo, serverId); + SessionContext context = new SessionContext(applicationPackage, sessionZKClient, sessionDir, applicationRepo, hostRegistry, superModelGenerationCounter); + return new LocalSession(tenant, sessionId, sessionPreparer, context); + } + + private long getLiveApp(ApplicationId applicationId) { + List<ApplicationId> applicationIds = applicationRepo.listApplications(); + if (applicationIds.contains(applicationId)) { + return applicationRepo.getSessionIdForApplication(applicationId); + } + return nonExistingActiveSession; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java new file mode 100644 index 00000000000..4057d010b15 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionPreparer.java @@ -0,0 +1,283 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.google.common.collect.ImmutableList; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.config.model.api.ModelContext; +import com.yahoo.config.provision.*; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.vespa.config.server.ApplicationSet; +import com.yahoo.vespa.config.server.ConfigServerSpec; +import com.yahoo.vespa.config.server.application.PermanentApplicationPackage; +import com.yahoo.vespa.config.server.modelfactory.ModelFactoryRegistry; +import com.yahoo.vespa.config.server.RotationsCache; +import com.yahoo.vespa.config.server.configchange.ConfigChangeActions; +import com.yahoo.vespa.config.server.deploy.ModelContextImpl; +import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer; +import com.yahoo.vespa.config.server.http.InvalidApplicationException; +import com.yahoo.vespa.config.server.modelfactory.PreparedModelsBuilder; +import com.yahoo.vespa.config.server.provision.HostProvisionerProvider; + +import com.yahoo.vespa.curator.Curator; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +/** + * A SessionPreparer is responsible for preparing a session given an application package. + * + * @author lulf + * @since 5.1 + */ +public class SessionPreparer { + + private static final Logger log = Logger.getLogger(SessionPreparer.class.getName()); + + private final ModelFactoryRegistry modelFactoryRegistry; + private final FileDistributionFactory fileDistributionFactory; + private final HostProvisionerProvider hostProvisionerProvider; + private final PermanentApplicationPackage permanentApplicationPackage; + private final ConfigserverConfig configserverConfig; + private final ConfigDefinitionRepo configDefinitionRepo; + private final Curator curator; + private final Zone zone; + + public SessionPreparer(ModelFactoryRegistry modelFactoryRegistry, + FileDistributionFactory fileDistributionFactory, + HostProvisionerProvider hostProvisionerProvider, + PermanentApplicationPackage permanentApplicationPackage, + ConfigserverConfig configserverConfig, + ConfigDefinitionRepo configDefinitionRepo, + Curator curator, + Zone zone) { + this.modelFactoryRegistry = modelFactoryRegistry; + this.fileDistributionFactory = fileDistributionFactory; + this.hostProvisionerProvider = hostProvisionerProvider; + this.permanentApplicationPackage = permanentApplicationPackage; + this.configserverConfig = configserverConfig; + this.configDefinitionRepo = configDefinitionRepo; + this.curator = curator; + this.zone = zone; + } + + /** + * Prepares a session (validates, builds model, writes to zookeeper and distributes files) + * + * @param context Contains classes needed to read/write session data. + * @param logger For storing logs returned in response to client. + * @param params parameters controlling behaviour of prepare. + * @param currentActiveApplicationSet Set of currently active applications. + * @param tenantPath Zookeeper path for the tenant for this session + * @return The config change actions that must be done to handle the activation of the models prepared. + */ + public ConfigChangeActions prepare(SessionContext context, DeployLogger logger, PrepareParams params, + Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) + { + Preparation prep = new Preparation(context, logger, params, currentActiveApplicationSet, tenantPath); + prep.preprocess(); + try { + prep.buildModels(); + prep.makeResult(); + if (!params.isDryRun()) { + prep.writeStateZK(); + prep.writeRotZK(); + prep.distribute(); + prep.reloadDeployFileDistributor(); + } + return prep.result(); + } catch (IllegalArgumentException e) { + throw new InvalidApplicationException("Invalid application package", e); + } + } + + private class Preparation { + + final SessionContext context; + final DeployLogger logger; + final PrepareParams params; + + final Optional<ApplicationSet> currentActiveApplicationSet; + final Path tenantPath; + final ApplicationId applicationId; + final RotationsCache rotationsCache; + final Set<Rotation> rotations; + final ModelContext.Properties properties; + + private ApplicationPackage applicationPackage; + private List<PreparedModelsBuilder.PreparedModelResult> modelResultList; + private PrepareResult prepareResult; + + private final PreparedModelsBuilder preparedModelsBuilder; + + Preparation(SessionContext context, DeployLogger logger, PrepareParams params, + Optional<ApplicationSet> currentActiveApplicationSet, Path tenantPath) { + this.context = context; + this.logger = logger; + this.params = params; + this.currentActiveApplicationSet = currentActiveApplicationSet; + this.tenantPath = tenantPath; + + this.applicationId = params.getApplicationId(); + this.rotationsCache = new RotationsCache(curator, tenantPath); + this.rotations = getRotations(params.rotations()); + this.properties = new ModelContextImpl.Properties(params.getApplicationId(), + configserverConfig.multitenant(), + ConfigServerSpec.fromConfig(configserverConfig), + configserverConfig.hostedVespa(), + zone, + rotations); + this.preparedModelsBuilder = new PreparedModelsBuilder(modelFactoryRegistry, + permanentApplicationPackage, + configserverConfig, + configDefinitionRepo, + curator, + zone, + fileDistributionFactory, + hostProvisionerProvider, + context, + logger, + params, + currentActiveApplicationSet, + tenantPath); + } + + void checkTimeout(String step) { + if (! params.getTimeoutBudget().hasTimeLeft()) { + String used = params.getTimeoutBudget().timesUsed(); + throw new RuntimeException("prepare timed out "+used+" after "+step+" step: " + applicationId); + } + } + + void preprocess() { + try { + this.applicationPackage = context.getApplicationPackage().preprocess( + properties.zone(), + null, + logger); + } catch (IOException | TransformerException | ParserConfigurationException | SAXException e) { + throw new RuntimeException("Error deploying application package", e); + } + checkTimeout("preprocess"); + } + + void buildModels() { + this.modelResultList = preparedModelsBuilder.buildModels(applicationId, applicationPackage); + checkTimeout("build models"); + } + + void makeResult() { + this.prepareResult = new PrepareResult(modelResultList); + checkTimeout("making result from models"); + } + + void writeStateZK() { + log.log(LogLevel.DEBUG, "Writing application package state to zookeeper"); + writeStateToZooKeeper(context.getSessionZooKeeperClient(), applicationPackage, params, logger, + prepareResult.getFileRegistries(), prepareResult.getProvisionInfos()); + checkTimeout("write state to zookeeper"); + } + + void writeRotZK() { + rotationsCache.writeRotationsToZooKeeper(applicationId, rotations); + checkTimeout("write rotations to zookeeper"); + } + + void distribute() { + prepareResult.asList().forEach(modelResult -> modelResult.model + .distributeFiles(modelResult.fileDistributionProvider.getFileDistribution())); + checkTimeout("distribute files"); + } + + void reloadDeployFileDistributor() { + if (prepareResult.asList().isEmpty()) return; + PreparedModelsBuilder.PreparedModelResult aModelResult = prepareResult.asList().get(0); + aModelResult.model.reloadDeployFileDistributor(aModelResult.fileDistributionProvider.getFileDistribution()); + checkTimeout("reload all deployed files in file distributor"); + } + + ConfigChangeActions result() { + return prepareResult.getConfigChangeActions(); + } + + private Set<Rotation> getRotations(Set<Rotation> rotations) { + if (rotations == null || rotations.isEmpty()) { + rotations = rotationsCache.readRotationsFromZooKeeper(applicationId); + } + return rotations; + } + + } + + private void writeStateToZooKeeper(SessionZooKeeperClient zooKeeperClient, + ApplicationPackage applicationPackage, + PrepareParams prepareParams, + DeployLogger deployLogger, + Map<Version, FileRegistry> fileRegistryMap, + Map<Version, ProvisionInfo> provisionInfoMap) { + ZooKeeperDeployer zkDeployer = zooKeeperClient.createDeployer(deployLogger); + try { + zkDeployer.deploy(applicationPackage, fileRegistryMap, provisionInfoMap); + zooKeeperClient.writeApplicationId(prepareParams.getApplicationId()); + } catch (RuntimeException | IOException e) { + zkDeployer.cleanup(); + throw new RuntimeException("Error preparing session", e); + } + } + + /** The result of preparation over all model versions */ + private static class PrepareResult { + + private final ImmutableList<PreparedModelsBuilder.PreparedModelResult> results; + + public PrepareResult(List<PreparedModelsBuilder.PreparedModelResult> results) { + this.results = ImmutableList.copyOf(results); + } + + /** Returns the results for each model as an immutable list */ + public List<PreparedModelsBuilder.PreparedModelResult> asList() { return results; } + + public Map<Version, ProvisionInfo> getProvisionInfos() { + return results.stream() + .filter(result -> result.model.getProvisionInfo().isPresent()) + .collect(Collectors.toMap((prepareResult -> prepareResult.version), + (prepareResult -> prepareResult.model.getProvisionInfo().get()))); + } + + public Map<Version, FileRegistry> getFileRegistries() { + return results.stream() + .collect(Collectors.toMap((prepareResult -> prepareResult.version), + (prepareResult -> prepareResult.fileDistributionProvider.getFileRegistry()))); + } + + /** + * Collects the config change actions from all model factory creations and returns the aggregated union of these actions. + * A system in the process of upgrading Vespa will have hosts running both version X and Y, and this will change + * during the upgrade process. Trying to be smart about which actions to perform on which hosts depending + * on the version running will be a nightmare to maintain. A pragmatic approach is therefore to just use the + * union of all actions as this will give the correct end result at the cost of perhaps restarting nodes twice + * (once for the upgrading case and once for a potential restart action). + */ + public ConfigChangeActions getConfigChangeActions() { + return new ConfigChangeActions(results.stream(). + map(result -> result.actions). + flatMap(actions -> actions.stream()). + collect(Collectors.toList())); + } + + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java new file mode 100644 index 00000000000..872e6117637 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionRepo.java @@ -0,0 +1,78 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.vespa.config.server.TimeoutBudget; +import com.yahoo.vespa.config.server.http.NotFoundException; + +import java.time.Clock; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; + +/** + * A generic session repository that can store any type of session that extends the abstract interface. + * + * @author lulf + * @since 5.1 + */ +public class SessionRepo<SESSIONTYPE extends Session> { + + private final HashMap<Long, SESSIONTYPE> sessions = new HashMap<>(); + + public synchronized void addSession(SESSIONTYPE session) { + final long sessionId = session.getSessionId(); + if (sessions.containsKey(sessionId)) { + throw new IllegalArgumentException("There already exists a session with id '" + sessionId + "'"); + } + sessions.put(sessionId, session); + } + + public synchronized void removeSession(long id) { + if ( ! sessions.containsKey(id)) { + throw new IllegalArgumentException("No such session exists '" + id + "'"); + } + sessions.remove(id); + } + + /** + * Gets a Session + * + * @param id session id + * @return a session belonging to the id supplied, or null if no session with the id was found + */ + public synchronized SESSIONTYPE getSession(long id) { + return sessions.get(id); + } + + /** + * Gets a Session with a timeout + * + * @param id session id + * @param timeoutInMillis timeout for getting session (loops and wait for session to show up if not found) + * @return a session belonging to the id supplied, or null if no session with the id was found + */ + public synchronized SESSIONTYPE getSession(long id, long timeoutInMillis) { + try { + return internalGetSession(id, timeoutInMillis); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while retrieving session with id " + id); + } + } + + private synchronized SESSIONTYPE internalGetSession(long id, long timeoutInMillis) throws InterruptedException { + TimeoutBudget timeoutBudget = new TimeoutBudget(Clock.systemUTC(), Duration.ofMillis(timeoutInMillis)); + do { + SESSIONTYPE session = getSession(id); + if (session != null) { + return session; + } + wait(100); + } while (timeoutBudget.hasTimeLeft()); + throw new NotFoundException("Unable to retrieve session with id " + id + " before timeout was reached"); + } + + public synchronized Collection<SESSIONTYPE> listSessions() { + return new ArrayList<>(sessions.values()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java new file mode 100644 index 00000000000..37dff639a35 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionStateWatcher.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.log.LogLevel; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.config.server.ReloadHandler; +import com.yahoo.vespa.config.server.monitoring.MetricUpdater; +import com.yahoo.vespa.curator.Curator; +import org.apache.curator.framework.recipes.cache.ChildData; +import org.apache.curator.framework.recipes.cache.NodeCacheListener; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +/** + * Watches one particular session (/vespa/config/apps/n/sessionState in ZK) + * The session must be in the session repo. + * + * @author vegardh + */ +public class SessionStateWatcher implements NodeCacheListener { + + private static final Logger log = Logger.getLogger(SessionStateWatcher.class.getName()); + private final Curator.FileCache fileCache; + private final ReloadHandler reloadHandler; + private final RemoteSession session; + private final MetricUpdater metrics; + private final Executor executor; + + public SessionStateWatcher(Curator.FileCache fileCache, ReloadHandler reloadHandler, RemoteSession session, MetricUpdater metrics) throws Exception { + executor = Executors.newSingleThreadExecutor(ThreadFactoryFactory.getThreadFactory(SessionStateWatcher.class.getName() + "-" + session)); + this.fileCache = fileCache; + this.reloadHandler = reloadHandler; + this.session = session; + this.metrics = metrics; + this.fileCache.start(); + this.fileCache.addListener(this); + } + + private void sessionChanged(Session.Status status) { + log.log(LogLevel.DEBUG, session.logPre()+"Session change: Session " + session.getSessionId() + " changed status to " + status); + + // valid for NEW -> PREPARE transitions, not ACTIVATE -> PREPARE. + if (status.equals(Session.Status.PREPARE)) { + log.log(LogLevel.DEBUG, session.logPre() + "Loading prepared session: " + session.getSessionId()); + session.loadPrepared(); + } else if (status.equals(Session.Status.ACTIVATE)) { + session.makeActive(reloadHandler); + } else if (status.equals(Session.Status.DEACTIVATE)) { + session.deactivate(); + } + } + + public long getSessionId() { + return session.getSessionId(); + } + + public void close() { + try { + fileCache.close(); + } catch (Exception e) { + log.log(LogLevel.WARNING, "Exception when closing watcher", e); + } + } + + @Override + public void nodeChanged() throws Exception { + executor.execute(() -> { + try { + ChildData data = fileCache.getCurrentData(); + if (data != null) { + sessionChanged(Session.Status.parse(Utf8.toString(fileCache.getCurrentData().getData()))); + } + } catch (Exception e) { + log.log(LogLevel.WARNING, session.logPre() + "Error handling session changed for session " + getSessionId(), e); + metrics.incSessionChangeErrors(); + } + }); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java new file mode 100644 index 00000000000..e17667c2a5a --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SessionZooKeeperClient.java @@ -0,0 +1,213 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.application.api.DeployLogger; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.TenantName; +import com.yahoo.transaction.Transaction; +import com.yahoo.log.LogLevel; +import com.yahoo.path.Path; +import com.yahoo.config.model.api.ConfigDefinitionRepo; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.config.server.StaticConfigDefinitionRepo; +import com.yahoo.vespa.config.server.ServerCache; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.config.server.deploy.ZooKeeperClient; +import com.yahoo.vespa.config.server.deploy.ZooKeeperDeployer; +import com.yahoo.vespa.config.server.zookeeper.ZKApplicationPackage; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.curator.transaction.CuratorOperations; +import com.yahoo.vespa.curator.transaction.CuratorTransaction; +import com.yahoo.vespa.config.server.zookeeper.ConfigCurator; + +import java.util.concurrent.TimeUnit; + +/** + * Zookeeper client for a specific session. Can be used to read and write session status + * and create and get prepare and active barrier. + * + * @author lulf + * @since 5.1 + */ +public class SessionZooKeeperClient { + + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(SessionZooKeeperClient.class.getName()); + static final String APPLICATION_ID_PATH = "applicationId"; + static final String CREATE_TIME_PATH = "createTime"; + private final Curator curator; + private final ConfigCurator configCurator; + private final Path rootPath; + private final Path sessionStatusPath; + private final String serverId; + private final ServerCacheLoader cacheLoader; + + // Only for testing when cache loader does not need cache entries. + public SessionZooKeeperClient(Curator curator, Path rootPath) { + this(curator, ConfigCurator.create(curator), rootPath, new StaticConfigDefinitionRepo(), ""); + } + + public SessionZooKeeperClient(Curator curator, ConfigCurator configCurator, Path rootPath, ConfigDefinitionRepo definitionRepo, String serverId) { + this.curator = curator; + this.configCurator = configCurator; + this.rootPath = rootPath; + this.serverId = serverId; + this.sessionStatusPath = rootPath.append(ConfigCurator.SESSIONSTATE_ZK_SUBPATH); + this.cacheLoader = new ServerCacheLoader(configCurator, rootPath, definitionRepo); + } + + public void writeStatus(Session.Status sessionStatus) { + try { + createWriteStatusTransaction(sessionStatus).commit(); + } catch (Exception e) { + throw new RuntimeException("Unable to write session status", e); + } + } + + public Session.Status readStatus() { + try { + String data = configCurator.getData(sessionStatusPath.getAbsolute()); + return Session.Status.parse(data); + } catch (Exception e) { + log.log(LogLevel.INFO, "Unable to read session status, assuming it was deleted"); + return Session.Status.NONE; + } + } + + Curator.CompletionWaiter createPrepareWaiter() { + return createCompletionWaiter(PREPARE_BARRIER); + } + + Curator.CompletionWaiter createActiveWaiter() { + return createCompletionWaiter(ACTIVE_BARRIER); + } + + Curator.CompletionWaiter getPrepareWaiter() { + return getCompletionWaiter(getWaiterPath(PREPARE_BARRIER)); + } + + Curator.CompletionWaiter getActiveWaiter() { + return getCompletionWaiter(getWaiterPath(ACTIVE_BARRIER)); + } + + Curator.CompletionWaiter getUploadWaiter() { + return getCompletionWaiter(getWaiterPath(UPLOAD_BARRIER)); + } + + private static final String PREPARE_BARRIER = "prepareBarrier"; + private static final String ACTIVE_BARRIER = "activeBarrier"; + private static final String UPLOAD_BARRIER = "uploadBarrier"; + + private Path getWaiterPath(String barrierName) { + return rootPath.append(barrierName); + } + + private int getNumberOfMembers() { + /* + * The number of members required in a barrier is the majority of servers. + */ + int numServers = curator.serverCount(); + return (numServers / 2) + 1; + } + + private Curator.CompletionWaiter createCompletionWaiter(String waiterNode) { + return curator.createCompletionWaiter(rootPath, waiterNode, getNumberOfMembers(), serverId); + } + + private Curator.CompletionWaiter getCompletionWaiter(Path path) { + return curator.getCompletionWaiter(path, getNumberOfMembers(), serverId); + } + + public void delete() { + try { + log.log(LogLevel.DEBUG, "Deleting " + rootPath.getAbsolute()); + configCurator.deleteRecurse(rootPath.getAbsolute()); + } catch (RuntimeException e) { + log.log(LogLevel.INFO, "Error deleting session (" + rootPath.getAbsolute() + ") from zookeeper"); + } + } + + public ApplicationPackage loadApplicationPackage() { + return new ZKApplicationPackage(configCurator, rootPath); + } + + public ServerCache loadServerCache() { + return cacheLoader.loadCache(); + } + + public void writeApplicationId(ApplicationId id) { + String path = getApplicationIdPath(); + try { + configCurator.putData(path, id.serializedForm()); + } catch (RuntimeException e) { + throw new RuntimeException("Unable to write application id '" + id + "' to '" + path + "'", e); + } + } + + private String getApplicationIdPath() { + return rootPath.append(APPLICATION_ID_PATH).getAbsolute(); + } + + public ApplicationId readApplicationId(TenantName tenant) { + String path = getApplicationIdPath(); + try { + // Fallback for cases where id never existed. + if ( ! configCurator.exists(path)) { + // TODO: DEBUG LOG + log.log(LogLevel.INFO, "Unable to locate application id at '" + path + "', returning default"); + return ApplicationId.defaultId(); + } + return ApplicationId.fromSerializedForm(tenant, configCurator.getData(path)); + } catch (RuntimeException e) { + throw new RuntimeException("Unable to read application id from '" + path + "'", e); + } + } + + // in seconds + public long readCreateTime() { + String path = getCreateTimePath(); + if (!configCurator.exists(path)) return 0l; + return Long.parseLong(configCurator.getData(path)); + } + + private String getCreateTimePath() { + return rootPath.append(CREATE_TIME_PATH).getAbsolute(); + } + + ProvisionInfo getProvisionInfo() { + return loadApplicationPackage().getProvisionInfoMap().values().stream() + .reduce((infoA, infoB) -> infoA.merge(infoB)) + .orElseThrow(() -> new IllegalStateException("Trying to read provision info, but no provision info exists")); + } + + public ZooKeeperDeployer createDeployer(DeployLogger logger) { + ZooKeeperClient zkClient = new ZooKeeperClient(configCurator, logger, true, rootPath); + return new ZooKeeperDeployer(zkClient); + } + + public Transaction createWriteStatusTransaction(Session.Status status) { + String path = sessionStatusPath.getAbsolute(); + CuratorTransaction transaction = new CuratorTransaction(curator); + if (configCurator.exists(path)) { + transaction.add(CuratorOperations.setData(sessionStatusPath.getAbsolute(), Utf8.toBytes(status.name()))); + } else { + transaction.add(CuratorOperations.create(sessionStatusPath.getAbsolute(), Utf8.toBytes(status.name()))); + } + return transaction; + } + + /** + * Create necessary paths atomically for a new session. + * @param createTime Time of session creation. + * @param timeUnit Time unit of createTime. + */ + public void createNewSession(long createTime, TimeUnit timeUnit) { + CuratorTransaction transaction = new CuratorTransaction(curator); + transaction.add(CuratorOperations.create(rootPath.getAbsolute())); + transaction.add(CuratorOperations.create(rootPath.append(UPLOAD_BARRIER).getAbsolute())); + transaction.add(createWriteStatusTransaction(Session.Status.NEW).operations()); + transaction.add(CuratorOperations.create(getCreateTimePath(), Utf8.toBytes(String.valueOf(timeUnit.toSeconds(createTime))))); + transaction.commit(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java new file mode 100644 index 00000000000..9121eab8d2f --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/session/SilentDeployLogger.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.session; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.yahoo.config.application.api.DeployLogger; + +/** + * The purpose of this is to mute the log messages from model and application building in {@link RemoteSession} that is triggered by {@link SessionStateWatcher}, since those messages already + * have been emitted by the prepare handler, for the same prepare operation. + * + * @author vegardh + * + */ +public class SilentDeployLogger implements DeployLogger { + + private static final Logger log = Logger.getLogger(SilentDeployLogger.class.getName()); + + @Override + public void log(Level level, String message) { + log.log(Level.FINE, message); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java b/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java new file mode 100644 index 00000000000..2a09e46d821 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/version/VersionState.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.version; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.provision.Version; +import com.yahoo.io.IOUtils; +import com.yahoo.vespa.defaults.Defaults; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; + +/** + * Contains version information for this configserver. + * + * @author lulf + */ +public class VersionState { + + private final File versionFile; + + @Inject + public VersionState(ConfigserverConfig config) { + this(new File(Defaults.getDefaults().underVespaHome(config.configServerDBDir()), "vespa_version")); + } + + public VersionState(File versionFile) { + this.versionFile = versionFile; + } + + public boolean isUpgraded() { + return currentVersion().compareTo(storedVersion()) > 0; + } + + public void saveNewVersion() { + try (FileWriter writer = new FileWriter(versionFile)) { + writer.write(currentVersion().toSerializedForm()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public Version storedVersion() { + try (FileReader reader = new FileReader(versionFile)) { + return Version.fromString(IOUtils.readAll(reader)); + } catch (Exception e) { + return Version.fromIntValues(0, 0, 0); // Use an old value to signal we don't know + } + } + + public Version currentVersion() { + return Version.fromIntValues(VespaVersion.major, VespaVersion.minor, VespaVersion.micro); + } + + @Override + public String toString() { + return String.format("Current version:%s, stored version:%s", currentVersion(), storedVersion()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java new file mode 100644 index 00000000000..af8d2fed95c --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ConfigCurator.java @@ -0,0 +1,429 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.google.inject.Inject; +import com.yahoo.cloud.config.ZookeeperServerConfig; +import com.yahoo.io.IOUtils; +import com.yahoo.log.LogLevel; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.curator.Curator; +import com.yahoo.vespa.zookeeper.ZooKeeperServer; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A (stateful) curator wrapper for the config server. This simplifies Curator method calls used by the config server + * and knows about how config content is mapped to node names and stored. + * <p> + * Usage details: + * Config ids are stored as foo#bar#c0 instead of foo/bar/c0, for simplicity. + * Keep the amount of domain-specific logic here to a minimum. + * Data for one application x is stored on this form: + * /vespa/config/apps/x/defconfigs + * /vespa/config/apps/x/userapp + * The different types of configs are stored on this form (ie. names of the ZK nodes under their respective + * paths): + * <p> + * Def configs are stored on the form name,version + * The user application structure is exactly the same as in the user's app dir during deploy. + * The current live app id (for example x) is stored in the node /vespa/config/liveapp + * It is updated outside this class, typically in config server during reload-config. + * Some methods have retries and/or reconnect. This is necessary because ZK will throw on certain scenarios, + * even though it will recover from it itself, @see http://wiki.apache.org/hadoop/ZooKeeper/ErrorHandling + * + * @author vegardh + * @author bratseth + * @since 5.0 + */ +public class ConfigCurator { + + /** Path for def files, under one app */ + public static final String DEFCONFIGS_ZK_SUBPATH = "/defconfigs"; + + /** Path for def files, under one app */ + public static final String USER_DEFCONFIGS_ZK_SUBPATH = "/userdefconfigs"; + + /** Path for metadata about an application */ + public static final String META_ZK_PATH = "/meta"; + + /** Path for the app package's dir structure, under one app */ + public static final String USERAPP_ZK_SUBPATH = "/userapp"; + + /** Path for session state */ + public static final String SESSIONSTATE_ZK_SUBPATH = "/sessionState"; + + protected static final FilenameFilter acceptsAllFileNameFilter = (dir, name) -> true; + + private final Curator curator; + + public static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(ConfigCurator.class.getName()); + + /** The number of zookeeper operations done with this ZKFacade instance */ + private final AtomicInteger operations = new AtomicInteger(); + + /** The number of zookeeper read operations done with this ZKFacade instance */ + private final AtomicInteger readOperations = new AtomicInteger(); + + /** The number of zookeeper write operations done with this ZKFacade instance */ + private final AtomicInteger writeOperations = new AtomicInteger(); + + /** The maximum size of a ZooKeeper node */ + private final int maxNodeSize; + + /** + * Sets up thread local zk access if not done before and returns a facade object + * + * @return a ZKFacade object + */ + public static ConfigCurator create(Curator curator, int juteMaxBuffer) { + return new ConfigCurator(curator, juteMaxBuffer); + } + + public static ConfigCurator create(Curator curator) { + return new ConfigCurator(curator, 1024*1024*10); + } + + @Inject + public ConfigCurator(Curator curator, ZooKeeperServer server) { + this(curator, server.getConfig().juteMaxBuffer()); + } + + private ConfigCurator(Curator curator, int maxNodeSize) { + this.curator = curator; + this.maxNodeSize = maxNodeSize; + log.log(LogLevel.CONFIG, "Using jute max buffer size " + this.maxNodeSize); + testZkConnection(); + } + + /** Returns the curator instance this wraps */ + public Curator curator() { return curator; } + + /** Cleans and creates a zookeeper completely */ + public void initAndClear(String path) { + try { + operations.incrementAndGet(); + readOperations.incrementAndGet(); + if (exists(path)) + deleteRecurse(path); + createRecurse(path); + } + catch (Exception e) { + throw new RuntimeException("Exception clearing path " + path + " in ZooKeeper", e); + } + } + + /** Creates a path. If the path already exists this does nothing. */ + private void createRecurse(String path) { + try { + if (exists(path)) return; + curator.framework().create().creatingParentsIfNeeded().forPath(path); + } + catch (Exception e) { + throw new RuntimeException("Exception creating path " + path + " in ZooKeeper", e); + } + } + + /** Returns the data at a path and node. Replaces / by # in node names. Returns null if the path doesn't exist. */ + public String getData(String path, String node) { + return getData(createFullPath(path, node)); + } + + /** Returns the data at a path. Returns null if the path doesn't exist. */ + public String getData(String path) { + byte[] data = getBytes(path); + return (data == null) ? null : Utf8.toString(data); + } + + /** Returns the data at a path and node. Replaces / by # in node names. Returns null if the path doesn't exist. */ + public byte[] getBytes(String path, String node) { + return getBytes(createFullPath(path, node)); + } + + /** + * Returns the data at a path, or null if the path does not exist. + * + * @param path a String with a pathname. + * @return a byte array with data. + */ + public byte[] getBytes(String path) { + try { + if ( ! exists(path)) return null; // TODO: Ugh + operations.incrementAndGet(); + readOperations.incrementAndGet(); + return curator.framework().getData().forPath(path); + } + catch (Exception e) { + throw new RuntimeException("Exception reading from path " + path + " in ZooKeeper", e); + } + } + + /** Returns whether a path exists in zookeeper */ + public boolean exists(String path, String node) { + return exists(createFullPath(path, node)); + } + + /** Returns whether a path exists in zookeeper */ + public boolean exists(String path) { + try { + return curator.framework().checkExists().forPath(path) != null; + } + catch (Exception e) { + throw new RuntimeException("Exception checking existence of path " + path + " in ZooKeeper", e); + } + } + + /** Creates a Zookeeper node. If the node already exists this does nothing. */ + public void createNode(String path) { + if ( ! exists(path)) + createRecurse(path); + operations.incrementAndGet(); + writeOperations.incrementAndGet(); + } + + /** Creates a Zookeeper node synchronously. Replaces / by # in node names. */ + public void createNode(String path, String node) { + createNode(createFullPath(path, node)); + } + + private String createFullPath(String path, String node) { + return path + "/" + toConfigserverName(node); + } + + /** Sets data at a given path and name. Replaces / by # in node names. Creates the node if it doesn't exist */ + public void putData(String path, String node, String data) { + putData(path, node, Utf8.toBytes(data)); + } + + /** Sets data at a given path. Creates the node if it doesn't exist */ + public void putData(String path, String data) { + putData(path, Utf8.toBytes(data)); + } + + private void ensureDataIsNotTooLarge(byte[] toPut, String path) { + if (toPut.length >= maxNodeSize) { + throw new IllegalArgumentException("Error: too much zookeeper data in node: " + + "[" + toPut.length + " bytes] (path " + path + ")"); + } + } + + /** Sets data at a given path and name. Replaces / by # in node names. Creates the node if it doesn't exist */ + public void putData(String path, String node, byte[] data) { + putData(createFullPath(path, node), data); + } + + /** Sets data at a given path. Creates the path if it doesn't exist */ + public void putData(String path, byte[] data) { + try { + ensureDataIsNotTooLarge(data, path); + operations.incrementAndGet(); + writeOperations.incrementAndGet(); + if (exists(path)) + curator.framework().setData().forPath(path, data); + else + curator.framework().create().creatingParentsIfNeeded().forPath(path, data); + } + catch (Exception e) { + throw new RuntimeException("Exception writing to path " + path + " in ZooKeeper", e); + } + } + + /** Sets data at an existing node. Replaces / by # in node names. */ + public void setData(String path, String node, String data) { + setData(path, node, Utf8.toBytes(data)); + } + + /** Sets data at an existing node. Replaces / by # in node names. */ + public void setData(String path, String node, byte[] data) { + setData(createFullPath(path, node), data); + } + + /** Sets data at an existing node. Replaces / by # in node names. */ + public void setData(String path, byte[] data) { + try { + ensureDataIsNotTooLarge(data, path); + operations.incrementAndGet(); + writeOperations.incrementAndGet(); + curator.framework().setData().forPath(path, data); + } + catch (Exception e) { + throw new RuntimeException("Exception writing to path " + path + " in ZooKeeper", e); + } + } + + /** + * Replaces / with # in the given node. + * + * @param node a zookeeper node name + * @return a config server node name + */ + protected String toConfigserverName(String node) { + if (node.startsWith("/")) node = node.substring(1); + return node.replaceAll("/", "#"); + } + + /** + * Lists thh children at the given path. + * + * @return the local names of the children at this path, or an empty list (never null) if none. + */ + public List<String> getChildren(String path) { + try { + operations.incrementAndGet(); + readOperations.incrementAndGet(); + return curator.framework().getChildren().forPath(path); + } + catch (Exception e) { + throw new RuntimeException("Exception getting children of path " + path + " in ZooKeeper", e); + } + } + + /** + * Puts config definition data and metadata into ZK. + * + * @param name The config definition name (including namespace) + * @param version The config definition version + * @param path /zoopath + * @param data The contents to write to ZK (as a byte array) + */ + public void putDefData(String name, String version, String path, byte[] data) { + if (version == null) { + putData(path, name, data); + } else { + String fullPath = createFullPath(path, name + "," + version); + if (exists(fullPath)) { + // TODO This should not happen when all the compatibility hacks in 5.1 have been removed + log.log(LogLevel.INFO, "There already exists a config definition '" + name + "', skipping feeding this one to ZooKeeper"); + } + else { + putData(fullPath, data); + } + } + } + + /** + * Takes for instance the dir /app and puts the contents into the given ZK path. Ignores files starting with dot, + * and dirs called CVS. + * + * @param dir directory which holds the summary class part files + * @param path zookeeper path + * @param filenameFilter A FilenameFilter which decides which files in dir are fed to zookeeper + * @param recurse recurse subdirectories + */ + public void feedZooKeeper(File dir, String path, FilenameFilter filenameFilter, boolean recurse) { + try { + if (filenameFilter == null) { + filenameFilter = acceptsAllFileNameFilter; + } + if (!dir.isDirectory()) { + log.fine(dir.getCanonicalPath() + " is not a directory. Not feeding the files into ZooKeeper."); + return; + } + for (File file : listFiles(dir, filenameFilter)) { + if (file.getName().startsWith(".")) continue; //.svn , .git ... + if ("CVS".equals(file.getName())) continue; + if (file.isFile()) { + byte[] contents = IOUtils.readFileBytes(file); + putData(path, file.getName(), contents); + } else if (recurse && file.isDirectory()) { + createNode(path, file.getName()); + feedZooKeeper(file, path + '/' + file.getName(), filenameFilter, recurse); + } + } + } + catch (IOException e) { + throw new RuntimeException("Exception feeding ZooKeeper at path " + path, e); + } + } + + /** + * Same as normal listFiles, but use the filter only for normal files + * + * @param dir directory to list files in + * @param filter A FilenameFilter which decides which files in dir are listed + * @return an array of Files + */ + protected File[] listFiles(File dir, FilenameFilter filter) { + File[] rawList = dir.listFiles(); + List<File> ret = new ArrayList<>(); + if (rawList != null) { + for (File f : rawList) { + if (f.isDirectory()) { + ret.add(f); + } else { + if (filter.accept(dir, f.getName())) { + ret.add(f); + } + } + } + } + return ret.toArray(new File[ret.size()]); + } + + /** + * The node string for a given config and path. Ignores id and/or version if the path is that of a + * data set that doesn't use id or version. + * + * @param path a zookeeper path + * @param name config definition name + * @param version config definition version + * @param id config id + * @return a String with path to a zookeeper node for a given config and path + */ + public static String getZkNodePath(String path, String name, String version, String id) { + if (path.endsWith(DEFCONFIGS_ZK_SUBPATH) || path.endsWith(USER_DEFCONFIGS_ZK_SUBPATH)) + return getConfigNodeName(name, version); + throw new IllegalArgumentException("Don't know how data in " + path + " is organised in ZK"); + } + + public static String getConfigNodeName(String name, String version) { + return getConfigNodeName(name, version, null); + } + + public static String getConfigNodeName(String name, String version, String configId) { + if (configId == null || "".equals(configId)) + return name + "," + version; + return name + "," + version + "," + configId; + } + + /** Deletes the node at the given path, and any children it may have. If the node does not exist this does nothing */ + public void deleteRecurse(String path) { + try { + if ( ! exists(path)) return; + curator.framework().delete().deletingChildrenIfNeeded().forPath(path); + } + catch (Exception e) { + throw new RuntimeException("Exception deleting path " + path, e); + } + } + + public Integer getNumberOfOperations() { return operations.intValue(); } + + public Integer getNumberOfReadOperations() { + return readOperations.intValue(); + } + + public Integer getNumberOfWriteOperations() { + return writeOperations.intValue(); + } + + + private void testZkConnection() { // This is not necessary, but allows us to give a useful error message + if (curator.connectionSpec().isEmpty()) return; + try { + curator.framework().checkExists().forPath("/dummy"); + } + catch (Exception e) { + log.log(LogLevel.ERROR, "Unable to contact ZooKeeper on " + curator.connectionSpec() + + ". Please verify for all configserver nodes that " + + "services.addr_configserver points to the correct configserver(s), " + + "the same configserver(s) as in services.xml, and that they are started. " + + "Check the log(s) for configserver errors. Aborting.", e); + } + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java new file mode 100644 index 00000000000..20572b938ba --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/InitializedCounter.java @@ -0,0 +1,88 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.curator.recipes.CuratorCounter; +import com.yahoo.vespa.curator.Curator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A counter that sets its initial value to the number of apps in zookeeper if no counter value is set. Subclass + * this to get that behavior. + * + * @author lulf + * @since 5.1 + */ +public class InitializedCounter { + + private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(InitializedCounter.class.getName()); + protected final CuratorCounter counter; + private final String sessionsDirPath; + + public InitializedCounter(Curator curator, String counterPath, String sessionsDirPath) { + this.sessionsDirPath = sessionsDirPath; + this.counter = new CuratorCounter(curator, counterPath); + initializeCounterValue(getLatestSessionId(ConfigCurator.create(curator), sessionsDirPath)); + } + + private void initializeCounterValue(Long latestSessionId) { + log.log(LogLevel.DEBUG, "path=" + sessionsDirPath + ", current=" + latestSessionId); + if (latestSessionId != null) { + counter.initialize(latestSessionId); + } else { + counter.initialize(1); + } + } + + /** + * Checks if an application exists in Zookeeper. + * + * @return true, if an application exists, false otherwise + */ + private static boolean applicationExists(ConfigCurator configCurator, String appsPath) { + // TODO Need to try and catch now since interface should not expose Zookeeper exceptions + try { + return configCurator.exists(appsPath); + } catch (Exception e) { + log.log(LogLevel.WARNING, e.getMessage()); + return false; + } + } + + /** + * Returns the application generation for the most recently deployed application from ZK, + * or null if no application has been deployed yet + * + * @return generation of the latest deployed application + */ + private static Long getLatestSessionId(ConfigCurator configCurator, String appsPath) { + if (!applicationExists(configCurator, appsPath)) return null; + Long newestGeneration = null; + try { + if (!getDeployedApplicationGenerations(configCurator, appsPath).isEmpty()) { + newestGeneration = Collections.max(getDeployedApplicationGenerations(configCurator, appsPath)); + } + } catch (Exception e) { + log.log(LogLevel.WARNING, "Could not get newest application generation from Zookeeper"); + } + return newestGeneration; + } + + private static List<Long> getDeployedApplicationGenerations(ConfigCurator configCurator, String appsPath) { + ArrayList<Long> generations = new ArrayList<>(); + try { + List<String> stringGenerations = configCurator.getChildren(appsPath); + if (stringGenerations != null && !(stringGenerations.isEmpty())) { + for (String s : stringGenerations) { + generations.add(Long.parseLong(s)); + } + } + } catch (RuntimeException e) { + log.log(LogLevel.WARNING, "Could not get application generations from Zookeeper"); + } + return generations; + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java new file mode 100644 index 00000000000..cad164f8614 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/SessionCounter.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.yahoo.path.Path; +import com.yahoo.vespa.curator.Curator; + +/** + * A counter keeping track of session ids in an atomic fashion across multiple config servers. + * + * @author lulf + * @since 5.1 + */ +public class SessionCounter extends InitializedCounter { + + public SessionCounter(Curator curator, Path rootPath, Path sessionsDir) { + super(curator, rootPath.append("sessionCounter").getAbsolute(), sessionsDir.getAbsolute()); + } + + /** + * Atomically increment and return next session id. + * + * @return a new session id. + */ + public long nextSessionId() { + return counter.next(); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java new file mode 100644 index 00000000000..4aedb487352 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationFile.java @@ -0,0 +1,175 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.path.Path; +import com.yahoo.io.IOUtils; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.config.util.ConfigUtils; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +/** + * @author lulf + * @author vegardh + * @since 5.1 + */ +class ZKApplicationFile extends ApplicationFile { + private static final Logger log = Logger.getLogger("ZKApplicationFile"); + private final ZKLiveApp zkApp; + private final ObjectMapper mapper = new ObjectMapper(); + + public ZKApplicationFile(Path path, ZKLiveApp app) { + super(path); + this.zkApp = app; + } + + @Override + public boolean isDirectory() { + String zkPath = getZKPath(path); + if (zkApp.exists(zkPath)) { + String data = zkApp.getData(zkPath); + if (data == null || data.isEmpty() || !zkApp.getChildren(zkPath).isEmpty()) { + return true; + } + } + return false; + } + + @Override + public boolean exists() { + try { + String zkPath = getZKPath(path); + return zkApp.exists(zkPath); + } catch (RuntimeException e) { + return false; + } + } + + @Override + public ApplicationFile delete() { + if (!listFiles().isEmpty()) { + throw new RuntimeException("Can't delete, directory not empty: " + this); + } + zkApp.deleteRecurse(getZKPath(path)); + writeMetaFile(null, ContentStatusDeleted); + return this; + } + + @Override + public Reader createReader() throws FileNotFoundException { + String zkPath = getZKPath(path); + String data = zkApp.getData(zkPath); + if (data == null) { + throw new FileNotFoundException("No such path: " + path); + } + return new StringReader(data); + } + + @Override + public InputStream createInputStream() throws FileNotFoundException { + String zkPath = getZKPath(path); + byte[] data = zkApp.getBytes(zkPath); + if (data == null) { + throw new FileNotFoundException("No such path: " + path); + } + return new ByteArrayInputStream(data); + } + + @Override + public ApplicationFile createDirectory() { + String zkPath = getZKPath(path); + if (isDirectory()) return this; + if (exists()) { + throw new IllegalArgumentException("Unable to create directory, file exists: " + path); + } + zkApp.create(zkPath); + writeMetaFile(null, ContentStatusNew); + return this; + } + + @Override + public ApplicationFile writeFile(Reader input) { + // foo/bar/baz.txt + String zkPath = getZKPath(path); + try { + String data = IOUtils.readAll(input); + String status = ContentStatusNew; + if (zkApp.exists(zkPath)) { + status = ContentStatusChanged; + } + zkApp.putData(zkPath, data); + writeMetaFile(data, status); + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + @Override + public List<ApplicationFile> listFiles(PathFilter filter) { + String userPath = getZKPath(path); + List<ApplicationFile> ret = new ArrayList<>(); + for (String zkChild : zkApp.getChildren(userPath)) { + Path childPath = path.append(zkChild); + // Ignore dot-files. + if (!childPath.getName().startsWith(".") && filter.accept(childPath)) { + ret.add(new ZKApplicationFile(childPath, zkApp)); + } + } + return ret; + } + + private static String getZKPath(Path path) { + if (path.isRoot()) { + return ConfigCurator.USERAPP_ZK_SUBPATH; + } + return ConfigCurator.USERAPP_ZK_SUBPATH + "/" + path.getRelative(); + } + + private void writeMetaFile(String input, String status) { + String metaPath = getZKPath(getMetaPath()); + StringWriter writer = new StringWriter(); + try { + mapper.writeValue(writer, new MetaData(status, input == null ? "" : ConfigUtils.getMd5(input))); + log.log(LogLevel.DEBUG, "Writing meta file to " + metaPath); + zkApp.putData(metaPath, writer.toString()); + } catch (IOException e) { + throw new RuntimeException("Error writing meta file to " + metaPath, e); + } + } + + public MetaData getMetaData() { + String metaPath = getZKPath(getMetaPath()); + log.log(LogLevel.DEBUG, "Getting metadata for " + metaPath); + if (!zkApp.exists(getZKPath(path))) { + if (zkApp.exists(metaPath)) { + return getMetaDataFromZk(metaPath); + } else { + return null; + } + } + if (zkApp.exists(metaPath)) { + return getMetaDataFromZk(metaPath); + } + return new MetaData(ContentStatusNew, isDirectory() ? "" : ConfigUtils.getMd5(zkApp.getData(getZKPath(path)))); + } + + private MetaData getMetaDataFromZk(String metaPath) { + try { + return mapper.readValue(zkApp.getBytes(metaPath), MetaData.class); + } catch (IOException e) { + return null; + } + } + + @Override + public int compareTo(ApplicationFile other) { + if (other == this) return 0; + return this.getPath().getName().compareTo((other).getPath().getName()); + } +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java new file mode 100644 index 00000000000..9061e47d134 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKApplicationPackage.java @@ -0,0 +1,281 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.google.common.base.Joiner; +import com.yahoo.config.application.api.ApplicationMetaData; +import com.yahoo.config.application.api.ComponentInfo; +import com.yahoo.config.application.api.FileRegistry; +import com.yahoo.config.application.api.UnparsedConfigDefinition; +import com.yahoo.config.codegen.DefParser; +import com.yahoo.config.application.api.ApplicationFile; +import com.yahoo.config.application.api.ApplicationPackage; +import com.yahoo.config.model.application.provider.*; +import com.yahoo.config.provision.ProvisionInfo; +import com.yahoo.config.provision.Version; +import com.yahoo.io.IOUtils; +import com.yahoo.path.Path; +import com.yahoo.io.reader.NamedReader; +import com.yahoo.vespa.config.ConfigDefinition; +import com.yahoo.vespa.config.ConfigDefinitionBuilder; +import com.yahoo.vespa.config.ConfigDefinitionKey; +import com.yahoo.vespa.config.util.ConfigUtils; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.*; + +/** + * Represents an application residing in zookeeper. + * + * @author tonytv + */ +public class ZKApplicationPackage implements ApplicationPackage { + + private ZKLiveApp liveApp; + + private final Map<Version, PreGeneratedFileRegistry> fileRegistryMap = new HashMap<>(); + private final Map<Version, ProvisionInfo> provisionInfoMap = new HashMap<>(); + private static final Version legacyVersion = Version.fromIntValues(0, 0, 0); + + public static final String fileRegistryNode = "fileregistry"; + public static final String allocatedHostsNode = "allocatedHosts"; + private final ApplicationMetaData metaData; + + public ZKApplicationPackage(ConfigCurator zk, Path appPath) { + verifyAppPath(zk, appPath); + liveApp = new ZKLiveApp(zk, appPath); + metaData = readMetaDataFromLiveApp(liveApp); + importFileRegistries(fileRegistryNode); + importProvisionInfos(allocatedHostsNode); + } + + private void importProvisionInfos(String allocatedHostsNode) { + List<String> provisionInfoNodes = liveApp.getChildren(allocatedHostsNode); + if (provisionInfoNodes.isEmpty()) { + Optional<ProvisionInfo> provisionInfo = importProvisionInfo(allocatedHostsNode); + provisionInfo.ifPresent(info -> provisionInfoMap.put(legacyVersion, info)); + } else { + provisionInfoNodes.stream() + .forEach(versionStr -> { + Version version = Version.fromString(versionStr); + Optional<ProvisionInfo> provisionInfo = importProvisionInfo(Joiner.on("/").join(allocatedHostsNode, versionStr)); + provisionInfo.ifPresent(info -> provisionInfoMap.put(version, info)); + }); + } + } + + private Optional<ProvisionInfo> importProvisionInfo(String provisionInfoNode) { + try { + if (liveApp.exists(provisionInfoNode)) { + return Optional.of(ProvisionInfo.fromJson(liveApp.getBytes(provisionInfoNode))); + } else { + return Optional.empty(); + } + } catch (Exception e) { + throw new RuntimeException("Unable to read provision info", e); + } + } + + private void importFileRegistries(String fileRegistryNode) { + List<String> fileRegistryNodes = liveApp.getChildren(fileRegistryNode); + if (fileRegistryNodes.isEmpty()) { + fileRegistryMap.put(legacyVersion, importFileRegistry(fileRegistryNode)); + } else { + fileRegistryNodes.stream() + .forEach(versionStr -> { + Version version = Version.fromString(versionStr); + fileRegistryMap.put(version, importFileRegistry(Joiner.on("/").join(fileRegistryNode, versionStr))); + }); + } + } + + private PreGeneratedFileRegistry importFileRegistry(String fileRegistryNode) { + try { + return PreGeneratedFileRegistry.importRegistry(liveApp.getDataReader(fileRegistryNode)); + } catch (Exception e) { + throw new RuntimeException("Could not determine which files to distribute. " + + "Please try redeploying the application", e); + } + } + + private ApplicationMetaData readMetaDataFromLiveApp(ZKLiveApp liveApp) { + String metaDataString = liveApp.getData(ConfigCurator.META_ZK_PATH); + if (metaDataString == null || metaDataString.isEmpty()) { + return null; + } + return ApplicationMetaData.fromJsonString(liveApp.getData(ConfigCurator.META_ZK_PATH)); + } + + @Override + public ApplicationMetaData getMetaData() { + return metaData; + } + + private static void verifyAppPath(ConfigCurator zk, Path appPath) { + if (!zk.exists(appPath.getAbsolute())) + throw new RuntimeException("App with path " + appPath + " does not exist"); + } + + @Override + public String getApplicationName() { + return metaData.getApplicationName(); + } + + @Override + public Reader getServices() { + return getUserAppData(SERVICES); + } + + @Override + public Reader getHosts() { + if (liveApp.exists(ConfigCurator.USERAPP_ZK_SUBPATH,HOSTS)) + return getUserAppData(HOSTS); + return null; + } + + @Override + public List<NamedReader> searchDefinitionContents() { + List<NamedReader> ret = new ArrayList<>(); + for (String sd : liveApp.getChildren(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR)) { + if (sd.endsWith(ApplicationPackage.SD_NAME_SUFFIX)) { + ret.add(new NamedReader(sd, new StringReader(liveApp.getData(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR, sd)))); + } + } + return ret; + } + + public Map<Version, ProvisionInfo> getProvisionInfoMap() { + return Collections.unmodifiableMap(provisionInfoMap); + } + + @Override + public Map<Version, FileRegistry> getFileRegistryMap() { + return Collections.unmodifiableMap(fileRegistryMap); + } + + private Optional<PreGeneratedFileRegistry> getPreGeneratedFileRegistry(Version vespaVersion) { + // Assumes at least one file registry, which we always have. + Optional<PreGeneratedFileRegistry> fileRegistry = Optional.ofNullable(fileRegistryMap.get(vespaVersion)); + if (!fileRegistry.isPresent()) { + fileRegistry = Optional.of(fileRegistryMap.values().iterator().next()); + } + return fileRegistry; + } + + @Override + public List<NamedReader> getSearchDefinitions() { + return searchDefinitionContents(); + } + + private Reader retrieveConfigDefReader(String def) { + try { + return liveApp.getDataReader(ConfigCurator.DEFCONFIGS_ZK_SUBPATH, def); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Could not retrieve config definition " + def + ".", e); + } + } + + @Override + public Map<ConfigDefinitionKey, UnparsedConfigDefinition> getAllExistingConfigDefs() { + Map<ConfigDefinitionKey, UnparsedConfigDefinition> ret = new LinkedHashMap<>(); + + List<String> allDefs = liveApp.getChildren(ConfigCurator.DEFCONFIGS_ZK_SUBPATH); + + for (final String nodeName : allDefs) { + final ConfigDefinitionKey key = ConfigUtils.createConfigDefinitionKeyFromZKString(nodeName); + ret.put(key, new UnparsedConfigDefinition() { + @Override + public ConfigDefinition parse() { + DefParser parser = new DefParser(key.getName(), retrieveConfigDefReader(nodeName)); + return ConfigDefinitionBuilder.createConfigDefinition(parser.getTree()); + } + + @Override + public String getUnparsedContent() { + try { + return IOUtils.readAll(retrieveConfigDefReader(nodeName)); + } catch (Exception e) { + throw new RuntimeException("Error retriving def file", e); + } + } + }); + } + return ret; + } + + //Returns readers for all the children of a node. + //The node is looked up relative to the location of the active application package + //in zookeeper. + @Override + public List<NamedReader> getFiles(Path relativePath,String suffix,boolean recurse) { + return liveApp.getAllDataFromDirectory(ConfigCurator.USERAPP_ZK_SUBPATH + '/' + relativePath.getRelative(), suffix, recurse); + } + + @Override + public ApplicationFile getFile(Path file) { // foo/bar/baz.json + return new ZKApplicationFile(file, liveApp); + } + + @Override + public String getHostSource() { + return "zookeeper hosts file"; + } + + @Override + public String getServicesSource() { + return "zookeeper services file"; + } + + @Override + public Optional<Reader> getDeployment() { return optionalFile(DEPLOYMENT_FILE.getName()); } + + @Override + public Optional<Reader> getValidationOverrides() { return optionalFile(VALIDATION_OVERRIDES.getName()); } + + private Optional<Reader> optionalFile(String file) { + if (liveApp.exists(ConfigCurator.USERAPP_ZK_SUBPATH, file)) + return Optional.of(getUserAppData(file)); + else + return Optional.empty(); + } + + @Override + public List<ComponentInfo> getComponentsInfo(Version vespaVersion) { + List<ComponentInfo> components = new ArrayList<>(); + PreGeneratedFileRegistry fileRegistry = getPreGeneratedFileRegistry(vespaVersion).get(); + for (String path : fileRegistry.getPaths()) { + if (path.startsWith(FilesApplicationPackage.COMPONENT_DIR + File.separator) && path.endsWith(".jar")) { + ComponentInfo component = new ComponentInfo(path); + components.add(component); + } + } + return components; + } + + private Reader getUserAppData(String node) { + return liveApp.getDataReader(ConfigCurator.USERAPP_ZK_SUBPATH, node); + } + + @Override + public Reader getRankingExpression(String name) { + return liveApp.getDataReader(ConfigCurator.USERAPP_ZK_SUBPATH+"/"+SEARCH_DEFINITIONS_DIR, name); + } + + @Override + public File getFileReference(Path pathRelativeToAppDir) { + String fileName = liveApp.getData(ConfigCurator.USERAPP_ZK_SUBPATH + "/" + pathRelativeToAppDir.getRelative()); + return new File(fileName); + } + + @Override + public void validateIncludeDir(String dirName) { + String fullPath = ConfigCurator.USERAPP_ZK_SUBPATH + "/" + dirName; + if (!liveApp.exists(fullPath)) { + throw new IllegalArgumentException("Cannot include directory '" + dirName + + "', as it does not exist in ZooKeeper!"); + } + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java new file mode 100644 index 00000000000..254203118f2 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/ZKLiveApp.java @@ -0,0 +1,208 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.config.server.zookeeper; + +import com.yahoo.io.reader.NamedReader; +import com.yahoo.path.Path; + +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Responsible for providing data from the currently live application subtree in zookeeper. + * (i.e. /vespa/config/apps/<id of currently active app>/) + * + * @author tonytv + */ +public class ZKLiveApp { + + private static final Logger log = Logger.getLogger(ZKLiveApp.class.getName()); + + private final ConfigCurator zk; + private final Path appPath; + + public ZKLiveApp(ConfigCurator zk, Path appPath) { + this.zk = zk; + this.appPath = appPath; + } + + /** + * Returns a list of the files (as readers) in the given path. The readers <b>must</b> + * be closed by the caller. + * + * @param path a path relative to the currently active application (i.e. /vespa/config/apps/<id of currently active app>/). + * @param fileNameSuffix the suffix of files to return, or null to return all + * @param recursive if true, all files from all subdirectories of this will also be returned + * @return the files in the given path, or an empty list (never null) if the directory does not exist or is empty. + * The list gets owned by the caller and can be modified freely. + */ + public List<NamedReader> getAllDataFromDirectory(String path, String fileNameSuffix, boolean recursive) { + return getAllDataFromDirectory(path, "", fileNameSuffix, recursive); + } + + /** + * As above, except + * + * @param namePrefix the prefix to prepend to the returned reader names + */ + private List<NamedReader> getAllDataFromDirectory(String path, String namePrefix, String fileNameSuffix, boolean recursive) { + String fullPath = getFullPath(path); + List<NamedReader> result = new ArrayList<>(); + List<String> children = getChildren(path); + + try { + for (String child : children) { + if (fileNameSuffix == null || child.endsWith(fileNameSuffix)) { + result.add(new NamedReader(namePrefix + child, reader(zk.getData(fullPath, child)))); + if (log.isLoggable(Level.FINER)) + log.finer("ZKApplicationPackage: Added '" + child + "' (matched suffix " + fileNameSuffix + ")"); + } else { + if (log.isLoggable(Level.FINER)) + log.finer("ZKApplicationPackage: Skipped '" + child + "' (did not match suffix " + fileNameSuffix + ")"); + } + if (recursive) + result.addAll(getAllDataFromDirectory(path + "/" + child, namePrefix + child + "/", fileNameSuffix, recursive)); + } + if (log.isLoggable(Level.FINE)) + log.fine("ZKApplicationPackage: Found '" + result.size() + "' files in " + fullPath); + return result; + } catch (Exception e) { + throw new RuntimeException("Could not retrieve all data from '" + fullPath + "' in zookeeper", e); + } + } + + /** + * Retrieves a node relative to the node of the live application, e.g. /vespa/config/apps/$lt;app_id>/<path>/<node> + * + * @param path a path relative to the currently active application + * @param node a path relative to the path above + * @return a Reader that can be used to get the data + */ + public Reader getDataReader(String path, String node) { + final String data = getData(path, node); + if (data == null) { + throw new IllegalArgumentException("No node for " + getFullPath(path) + "/" + node + " exists"); + } + return reader(data); + } + + public String getData(String path, String node) { + try { + return zk.getData(getFullPath(path), node); + } catch (Exception e) { + throw new IllegalArgumentException("Could not retrieve node '" + getFullPath(path) + "/" + node + "' in zookeeper", e); + } + } + + public String getData(String path) { + try { + return zk.getData(getFullPath(path)); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Could not retrieve path '" + getFullPath(path) + "' in zookeeper", e); + } + } + + public byte[] getBytes(String path) { + try { + return zk.getBytes(getFullPath(path)); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Could not retrieve path '" + getFullPath(path) + "' in zookeeper", e); + } + } + + public void putData(String path, String data) { + try { + zk.putData(getFullPath(path), data); + } catch (RuntimeException e) { + throw new IllegalArgumentException("Could not put data to node '" + getFullPath(path) + "' in zookeeper", e); + } + } + + public void create(String path, String node) { + if (path != null && !path.startsWith("/")) path = "/" + path; + try { + zk.createNode(getFullPath(path), node); + } catch (RuntimeException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Checks if the given node exists under path under this live app + * + * @param path a zookeeper path + * @param node a zookeeper node + * @return true if the node exists in the path, false otherwise + */ + public boolean exists(String path, String node) { + return zk.exists(getFullPath(path), node); + } + + /** + * Checks if the given node exists under path under this live app + * + * @param path a zookeeper path + * @return true if the node exists in the path, false otherwise + */ + public boolean exists(String path) { + return zk.exists(getFullPath(path)); + } + + private String getFullPath(String path) { + Path fullPath = appPath; + if (path != null) { + fullPath = appPath.append(path); + } + return fullPath.getAbsolute(); + } + + /** + * Recursively delete given path + * + * @param path path to delete + */ + public void deleteRecurse(String path) { + zk.deleteRecurse(getFullPath(path)); + } + + /** + * Returns the full list of children (file names) in the given path. + * + * @param path a path relative to the currently active application + * @return a list of file names + */ + public List<String> getChildren(String path) { + String fullPath = getFullPath(path); + if (! zk.exists(fullPath)) { + log.fine("ZKApplicationPackage: " + fullPath + " is not a valid dir"); + return Collections.emptyList(); + } + return zk.getChildren(fullPath); + } + + private static Reader reader(String string) { + return new StringReader(string); + } + + public void create(String path) { + if (path != null && !path.startsWith("/")) path = "/" + path; + try { + zk.createNode(getFullPath(path)); + } catch (RuntimeException e) { + throw new IllegalArgumentException(e); + } + } + + public Reader getDataReader(String path) { + final String data = getData(path); + if (data == null) { + throw new IllegalArgumentException("No node for " + getFullPath(path) + " exists"); + } + return reader(data); + } +} + diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java new file mode 100644 index 00000000000..a548df4d493 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/zookeeper/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author tonytv + */ +@ExportPackage +package com.yahoo.vespa.config.server.zookeeper; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java new file mode 100644 index 00000000000..879dae419d2 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/Cluster.java @@ -0,0 +1,85 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import java.util.Arrays; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Model a single cluster of services in the Vespa model. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public final class Cluster implements Comparable<Cluster> { + @NonNull + public final String name; + @NonNull + public final String type; + /** + * An ordered list of the service instances in this cluster. + */ + @NonNull + public final ImmutableList<Service> services; + + public Cluster(String name, String type, List<Service> services) { + this.name = name; + this.type = type; + ImmutableList.Builder<Service> builder = ImmutableList.builder(); + Service[] sortingBuffer = services.toArray(new Service[0]); + Arrays.sort(sortingBuffer); + builder.add(sortingBuffer); + this.services = builder.build(); + } + + @Override + public int compareTo(Cluster other) { + int nameOrder = name.compareTo(other.name); + if (nameOrder != 0) { + return nameOrder; + } + return type.compareTo(other.type); + } + + @Override + public int hashCode() { + final int prime = 761; + int result = 1; + result = prime * result + name.hashCode(); + result = prime * result + type.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Cluster other = (Cluster) obj; + if (!name.equals(other.name)) { + return false; + } + if (!type.equals(other.type)) { + return false; + } + return true; + } + + @Override + public String toString() { + final int maxLen = 3; + StringBuilder builder = new StringBuilder(); + builder.append("Cluster [name=").append(name).append(", type=").append(type).append(", services=") + .append(services.subList(0, Math.min(services.size(), maxLen))).append("]"); + return builder.toString(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java new file mode 100644 index 00000000000..163743cb1bb --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ConfigServerLocation.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import com.yahoo.cloud.config.ConfigserverConfig; + +/** + * Wrapper for settings from the cloud.config.configserver config. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public class ConfigServerLocation { + public final int restApiPort; + + public ConfigServerLocation(ConfigserverConfig configServer) { + restApiPort = configServer.httpport(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("ConfigServerLocation [restApiPort=").append(restApiPort).append("]"); + return builder.toString(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java new file mode 100644 index 00000000000..60055df88df --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ProxyErrorMapper.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +/** + * Convert exceptions thrown by the internal REST client into a little more helpful responses. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +@Provider +public class ProxyErrorMapper implements ExceptionMapper<WebApplicationException> { + + @Override + public Response toResponse(WebApplicationException exception) { + StringBuilder msg = new StringBuilder("Invoking (external) web service failed: "); + msg.append(exception.getMessage()); + return Response.status(500).entity(msg.toString()).type("text/plain").build(); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java new file mode 100644 index 00000000000..4d0dc36a0c7 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/Service.java @@ -0,0 +1,172 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; +import com.yahoo.text.Utf8; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Model a single service instance as a sortable object. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public final class Service implements Comparable<Service> { + @NonNull + public final String serviceType; + @NonNull + public final String host; + public final int statePort; + @NonNull + public final String configId; + @NonNull + public final List<Integer> ports; + @NonNull + public final String name; + + public Service(String serviceType, String host, int statePort, String clusterName, String clusterType, + String configId, List<Integer> ports, String name) { + this.serviceType = serviceType; + this.host = host.toLowerCase(); + this.statePort = statePort; + this.configId = configId; + ImmutableList.Builder<Integer> portsBuilder = new ImmutableList.Builder<>(); + portsBuilder.addAll(ports); + this.ports = portsBuilder.build(); + this.name = name; + } + + @Override + public int compareTo(Service other) { + int serviceTypeOrder = serviceType.compareTo(other.serviceType); + if (serviceTypeOrder != 0) { + return serviceTypeOrder; + } + int hostOrder = host.compareTo(other.host); + if (hostOrder != 0) { + return hostOrder; + } + return Integer.compare(statePort, other.statePort); + } + + /** + * Generate an identifier string for one of the ports of this service + * suitable for using in an URL. + * + * @param port + * port which this identifier pertains to + * @return an opaque identifier string for this service + */ + public String getIdentifier(int port) { + StringBuilder b = new StringBuilder(serviceType); + b.append("-"); + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 should by definition always be available in the JVM.", e); + } + md5.update(Utf8.toBytes(serviceType)); + md5.update(Utf8.toBytes(configId)); + md5.update(Utf8.toBytes(host)); + for (int i = 3; i >= 0; --i) { + md5.update((byte) (port >>> i)); + } + byte[] digest = md5.digest(); + BigInteger digestMarshal = new BigInteger(1, digest); + b.append(digestMarshal.toString(36)); + return b.toString(); + } + + /** + * All valid identifiers for this object. + * + * @return a list with a unique ID for each of this service's ports + */ + public List<String> getIdentifiers() { + List<String> ids = new ArrayList<>(ports.size()); + for (int port : ports) { + ids.add(getIdentifier(port)); + } + return ids; + } + + /** + * Find which port number a hash code pertains to. + * + * @param identifier a string generated from {@link #getIdentifier(int)} + * @return a port number, or 0 if no match is found + */ + public int matchIdentifierWithPort(String identifier) { + for (int port : ports) { + if (identifier.equals(getIdentifier(port))) { + return port; + } + } + throw new IllegalArgumentException("Identifier " + identifier + " matches no ports in " + this); + } + + @Override + public String toString() { + final int maxLen = 3; + StringBuilder builder = new StringBuilder(); + builder.append("Service [serviceType=").append(serviceType).append(", host=").append(host).append(", statePort=") + .append(statePort).append(", configId=").append(configId).append(", ports=") + .append(ports.subList(0, Math.min(ports.size(), maxLen))).append(", name=").append(name) + .append("]"); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 131; + int result = 1; + result = prime * result + configId.hashCode(); + result = prime * result + host.hashCode(); + result = prime * result + name.hashCode(); + result = prime * result + ports.hashCode(); + result = prime * result + serviceType.hashCode(); + result = prime * result + statePort; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Service other = (Service) obj; + if (!configId.equals(other.configId)) { + return false; + } + if (!host.equals(other.host)) { + return false; + } + if (!name.equals(other.name)) { + return false; + } + if (!ports.equals(other.ports)) { + return false; + } + if (!serviceType.equals(other.serviceType)) { + return false; + } + if (statePort != other.statePort) { + return false; + } + return true; + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java new file mode 100644 index 00000000000..12141366811 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/ServiceModel.java @@ -0,0 +1,236 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Table; +import com.google.common.collect.Table.Cell; +import com.yahoo.vespa.serviceview.bindings.ApplicationView; +import com.yahoo.vespa.serviceview.bindings.ClusterView; +import com.yahoo.vespa.serviceview.bindings.HostService; +import com.yahoo.vespa.serviceview.bindings.ModelResponse; +import com.yahoo.vespa.serviceview.bindings.ServicePort; +import com.yahoo.vespa.serviceview.bindings.ServiceView; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A transposed view for cloud.config.model. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +public final class ServiceModel { + private static final String CLUSTERCONTROLLER_TYPENAME = "container-clustercontroller"; + + private static final String CONTENT_CLUSTER_TYPENAME = "content"; + + private final Map<String, Service> servicesMap; + + /** + * An ordered list of the clusters in this config model. + */ + @NonNull + public final ImmutableList<Cluster> clusters; + + ServiceModel(ModelResponse modelConfig) { + Table<String, String, List<Service>> services = HashBasedTable.create(); + for (HostService h : modelConfig.hosts) { + String hostName = h.name; + for (com.yahoo.vespa.serviceview.bindings.Service s : h.services) { + addService(services, hostName, s); + } + } + List<Cluster> sortingBuffer = new ArrayList<>(); + for (Cell<String, String, List<Service>> c : services.cellSet()) { + sortingBuffer.add(new Cluster(c.getRowKey(), c.getColumnKey(), c.getValue())); + } + Collections.sort(sortingBuffer); + ImmutableList.Builder<Cluster> clustersBuilder = new ImmutableList.Builder<>(); + clustersBuilder.addAll(sortingBuffer); + clusters = clustersBuilder.build(); + Map<String, Service> seenIdentifiers = new HashMap<>(); + for (Cluster c : clusters) { + for (Service s : c.services) { + List<String> identifiers = s.getIdentifiers(); + for (String identifier : identifiers) { + if (seenIdentifiers.containsKey(identifier)) { + throw new RuntimeException( + "Congrats, you have a publishable result. We have a very unexpected hash collision" + " between " + + seenIdentifiers.get(identifier) + " and " + s + "."); + } + seenIdentifiers.put(identifier, s); + } + } + } + ImmutableMap.Builder<String, Service> servicesBuilder = new ImmutableMap.Builder<>(); + servicesBuilder.putAll(seenIdentifiers); + servicesMap = servicesBuilder.build(); + } + + private static void addService(Table<String, String, List<Service>> services, + String hostName, + com.yahoo.vespa.serviceview.bindings.Service s) { + boolean hasStateApi = false; + int statePort = 0; + List<Integer> ports = new ArrayList<>(s.ports.size()); + for (ServicePort port : s.ports) { + ports.add(port.number); + if (!hasStateApi && port.hasTags("http", "state")) { + hasStateApi = true; + statePort = port.number; + } + } + // ignore hosts without state API + if (hasStateApi) { + Service service = new Service(s.type, hostName, statePort, s.clustername, s.clustertype, s.configid, ports, s.name); + getAndSetEntry(services, s.clustername, s.clustertype).add(service); + } + } + + private static List<Service> getAndSetEntry(Table<String, String, List<Service>> services, String clusterName, String clusterType) { + List<Service> serviceList = services.get(clusterName, clusterType); + if (serviceList == null) { + serviceList = new ArrayList<>(); + services.put(clusterName, clusterType, serviceList); + } + return serviceList; + } + + /** + * The top level view of a given application. + * + * @return a top level view of the entire application in a form suitable for + * consumption by a REST API + */ + public ApplicationView showAllClusters(String uriBase, String applicationIdentifier) { + ApplicationView response = new ApplicationView(); + List<ClusterView> clusterViews = new ArrayList<>(); + for (Cluster c : clusters) { + clusterViews.add(showCluster(c, uriBase, applicationIdentifier)); + } + response.clusters = clusterViews; + return response; + } + + private ClusterView showCluster(Cluster c, String uriBase, String applicationIdentifier) { + List<ServiceView> services = new ArrayList<>(); + for (Service s : c.services) { + ServiceView service = new ServiceView(); + StringBuilder buffer = getLinkBuilder(uriBase).append(applicationIdentifier).append('/'); + service.url = buffer.append("service/").append(s.getIdentifier(s.statePort)).append("/state/v1/").toString(); + service.serviceType = s.serviceType; + service.serviceName = s.name; + service.configId = s.configId; + service.host = s.host; + addLegacyLink(uriBase, applicationIdentifier, s, service); + services.add(service); + } + ClusterView v = new ClusterView(); + v.services = services; + v.name = c.name; + v.type = c.type; + if (CONTENT_CLUSTER_TYPENAME.equals(c.type)) { + Service s = getFirstClusterController(); + StringBuilder buffer = getLinkBuilder(uriBase).append(applicationIdentifier).append('/'); + buffer.append("service/").append(s.getIdentifier(s.statePort)).append("/cluster/v2/").append(c.name); + v.url = buffer.toString(); + } else { + v.url = null; + } + return v; + } + + private void addLegacyLink(String uriBase, String applicationIdentifier, Service s, ServiceView service) { + if (s.serviceType.equals("storagenode") || s.serviceType.equals("distributor")) { + StringBuilder legacyBuffer = getLinkBuilder(uriBase); + legacyBuffer.append("legacy/").append(applicationIdentifier).append('/'); + legacyBuffer.append("service/").append(s.getIdentifier(s.statePort)).append('/'); + service.legacyStatusPages = legacyBuffer.toString(); + } + } + + private Service getFirstServiceInstanceByType(@NonNull String typeName) { + for (Cluster c : clusters) { + for (Service s : c.services) { + if (typeName.equals(s.serviceType)) { + return s; + } + } + } + throw new IllegalStateException("This installation has but no service of required type: " + + typeName + "."); + } + + private Service getFirstClusterController() { + // This is used assuming all cluster controllers know of all fleet controllers in an application + return getFirstServiceInstanceByType(CLUSTERCONTROLLER_TYPENAME); + } + + private StringBuilder getLinkBuilder(String uriBase) { + StringBuilder buffer = new StringBuilder(uriBase); + if (!uriBase.endsWith("/")) { + buffer.append('/'); + } + return buffer; + } + + @Override + public String toString() { + final int maxLen = 3; + StringBuilder builder = new StringBuilder(); + builder.append("ServiceModel [clusters=") + .append(clusters.subList(0, Math.min(clusters.size(), maxLen))).append("]"); + return builder.toString(); + } + + + /** + * Match an identifier with a service for this cluster. + * + * @param identifier + * an opaque service identifier generated by the service + * @return the corresponding Service instance + */ + public Service getService(String identifier) { + return servicesMap.get(identifier); + } + + /** + * Find a service based on host and port. + * + * @param host + * the name of the host running the service + * @param port + * a port owned by the service + * @param self + * the service which generated the host data + * @return a service instance fullfilling the criteria + * @throws IllegalArgumentException + * if no matching service is found + */ + public Service resolve(String host, int port, Service self) { + Integer portAsObject = Integer.valueOf(port); + String realHost; + if ("localhost".equals(host)) { + realHost = self.host; + } else { + realHost = host; + } + for (Cluster c : clusters) { + for (Service s : c.services) { + if (s.host.equals(realHost) && s.ports.contains(portAsObject)) { + return s; + } + } + } + throw new IllegalArgumentException("No registered service owns port " + port + " on host " + realHost + "."); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java new file mode 100644 index 00000000000..c66e0c2b3ba --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/StateResource.java @@ -0,0 +1,282 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.serviceview; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import com.yahoo.container.jaxrs.annotation.Component; +import com.yahoo.vespa.serviceview.bindings.ApplicationView; +import com.yahoo.vespa.serviceview.bindings.ConfigClient; +import com.yahoo.vespa.serviceview.bindings.HealthClient; +import com.yahoo.vespa.serviceview.bindings.ModelResponse; +import com.yahoo.vespa.serviceview.bindings.StateClient; + +import org.glassfish.jersey.client.proxy.WebResourceFactory; + + +/** + * A web service to discover and proxy Vespa service state info. + * + * @author <a href="mailto:steinar@yahoo-inc.com">Steinar Knutsen</a> + */ +@Path("/") +public class StateResource implements StateClient { + private static final String SINGLE_API_LINK = "url"; + private final int restApiPort; + private final String host; + private final UriInfo uriInfo; + + @SuppressWarnings("serial") + private static class GiveUpLinkRetargetingException extends Exception { + public GiveUpLinkRetargetingException(Throwable reason) { + super(reason); + } + + public GiveUpLinkRetargetingException(String message) { + super(message); + } + } + + public StateResource(@Component ConfigServerLocation configServer, @Context UriInfo ui) { + this.restApiPort = configServer.restApiPort; + host = "localhost"; + this.uriInfo = ui; + } + + @Override + @GET + @Path("v1/") + @Produces(MediaType.APPLICATION_JSON) + public ApplicationView getDefaultUserInfo() { + return getUserInfo("default", "default", "default", "default", "default"); + } + + @Override + @GET + @Path("v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}") + @Produces(MediaType.APPLICATION_JSON) + public ApplicationView getUserInfo(@PathParam("tenantName") String tenantName, + @PathParam("applicationName") String applicationName, + @PathParam("environmentName") String environmentName, + @PathParam("regionName") String regionName, + @PathParam("instanceName") String instanceName) { + ServiceModel model = new ServiceModel( + getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName)); + return model.showAllClusters( + getBaseUri() + "v1/", + applicationIdentifier(tenantName, applicationName, environmentName, regionName, instanceName)); + } + + + @Produces(MediaType.TEXT_HTML) + public interface HtmlProxyHack { + @GET + public String proxy(); + } + + @GET + @Path("v1/legacy/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{apiParams: .*}") + @Produces(MediaType.TEXT_HTML) + public String htmlProxy(@PathParam("tenantName") String tenantName, + @PathParam("applicationName") String applicationName, + @PathParam("environmentName") String environmentName, + @PathParam("regionName") String regionName, + @PathParam("instanceName") String instanceName, + @PathParam("serviceIdentifier") String identifier, + @PathParam("apiParams") String apiParams) { + ServiceModel model = new ServiceModel(getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName)); + Service s = model.getService(identifier); + int requestedPort = s.matchIdentifierWithPort(identifier); + Client client = ClientBuilder.newClient(); + try { + final StringBuilder uriBuffer = new StringBuilder("http://").append(s.host).append(':').append(requestedPort).append('/') + .append(apiParams); + addQuery(uriBuffer); + String uri = uriBuffer.toString(); + WebTarget target = client.target(uri); + HtmlProxyHack resource = WebResourceFactory.newResource(HtmlProxyHack.class, target); + return resource.proxy(); + } finally { + if (client != null) { + client.close(); + } + } + } + + private String getBaseUri() { + String baseUri = uriInfo.getBaseUri().toString(); + if (baseUri.endsWith("/")) { + return baseUri; + } else { + return baseUri + "/"; + } + } + + protected ModelResponse getModelConfig(String tenant, String application, String environment, String region, String instance) { + Client client = ClientBuilder.newClient(); + try { + WebTarget target = client.target("http://" + host + ":" + restApiPort + "/"); + + ConfigClient resource = WebResourceFactory.newResource(ConfigClient.class, target); + + return resource.getServiceModel(tenant, application, environment, region, instance); + } finally { + if (client != null) { + client.close(); + } + } + } + + @SuppressWarnings("rawtypes") + @Override + @GET + @Path("v1/tenant/{tenantName}/application/{applicationName}/environment/{environmentName}/region/{regionName}/instance/{instanceName}/service/{serviceIdentifier}/{apiParams: .*}") + @Produces(MediaType.APPLICATION_JSON) + public HashMap singleService(@PathParam("tenantName") String tenantName, + @PathParam("applicationName") String applicationName, + @PathParam("environmentName") String environmentName, + @PathParam("regionName") String regionName, + @PathParam("instanceName") String instanceName, + @PathParam("serviceIdentifier") String identifier, + @PathParam("apiParams") String apiParams) { + ServiceModel model = new ServiceModel(getModelConfig(tenantName, applicationName, environmentName, regionName, instanceName)); + Service s = model.getService(identifier); + int requestedPort = s.matchIdentifierWithPort(identifier); + Client client = ClientBuilder.newClient(); + try { + HealthClient resource = getHealthClient(apiParams, s, requestedPort, client); + HashMap<?, ?> apiResult = resource.getHealthInfo(); + rewriteResourceLinks(apiResult, model, s, applicationIdentifier(tenantName, applicationName, environmentName, regionName, instanceName), identifier); + return apiResult; + } finally { + if (client != null) { + client.close(); + } + } + } + + protected HealthClient getHealthClient(String apiParams, Service s, int requestedPort, Client client) { + final StringBuilder uriBuffer = new StringBuilder("http://").append(s.host).append(':').append(requestedPort).append('/') + .append(apiParams); + addQuery(uriBuffer); + WebTarget target = client.target(uriBuffer.toString()); + return WebResourceFactory.newResource(HealthClient.class, target); + } + + private String applicationIdentifier(String tenant, String application, String environment, String region, String instance) { + return new StringBuilder("tenant/").append(tenant).append("/application/").append(application).append("/environment/") + .append(environment).append("/region/").append(region).append("/instance/").append(instance).toString(); + } + + private void rewriteResourceLinks(Object apiResult, + ServiceModel model, + Service self, + String applicationIdentifier, + String incomingIdentifier) { + if (apiResult instanceof List) { + for (@SuppressWarnings("unchecked") ListIterator<Object> i = ((List<Object>) apiResult).listIterator(); i.hasNext();) { + Object resource = i.next(); + if (resource instanceof String) { + try { + StringBuilder buffer = linkBuffer(applicationIdentifier); + // if it points to a port and host not part of the application, rewriting will not occur, so this is kind of safe + retarget(model, self, buffer, (String) resource); + i.set(buffer.toString()); + } catch (GiveUpLinkRetargetingException e) { + break; // assume relatively homogenous lists when doing rewrites to avoid freezing up on scanning long lists + } + } else { + rewriteResourceLinks(resource, model, self, applicationIdentifier, incomingIdentifier); + } + } + } else if (apiResult instanceof Map) { + @SuppressWarnings("unchecked") + Map<Object, Object> api = (Map<Object, Object>) apiResult; + for (Map.Entry<Object, Object> entry : api.entrySet()) { + if (SINGLE_API_LINK.equals(entry.getKey()) && entry.getValue() instanceof String) { + try { + rewriteSingleLink(entry, model, self, linkBuffer(applicationIdentifier)); + } catch (GiveUpLinkRetargetingException e) { + // NOP + } + } else if ("link".equals(entry.getKey()) && entry.getValue() instanceof String) { + buildSingleLink(entry, model, linkBuffer(applicationIdentifier), incomingIdentifier); + } else { + rewriteResourceLinks(entry.getValue(), model, self, applicationIdentifier, incomingIdentifier); + } + } + } + } + + private void buildSingleLink(Map.Entry<Object, Object> entry, + ServiceModel model, + StringBuilder newUri, + String incomingIdentifier) { + newUri.append("/service/") + .append(incomingIdentifier); + newUri.append(entry.getValue()); + entry.setValue(newUri.toString()); + } + + private void addQuery(StringBuilder newUri) { + String query = uriInfo.getRequestUri().getRawQuery(); + if (query != null && query.length() > 0) { + newUri.append('?').append(query); + } + } + + private StringBuilder linkBuffer(String applicationIdentifier) { + StringBuilder newUri = new StringBuilder(getBaseUri()); + newUri.append("v1/").append(applicationIdentifier); + return newUri; + } + + private void rewriteSingleLink(Map.Entry<Object, Object> entry, + ServiceModel model, + Service self, + StringBuilder newUri) throws GiveUpLinkRetargetingException { + String url = (String) entry.getValue(); + retarget(model, self, newUri, url); + entry.setValue(newUri.toString()); + } + + private void retarget(ServiceModel model, Service self, StringBuilder newUri, String url) throws GiveUpLinkRetargetingException { + URI link; + try { + link = new URI(url); + } catch (URISyntaxException e) { + throw new GiveUpLinkRetargetingException(e); + } + if (!link.isAbsolute()) { + throw new GiveUpLinkRetargetingException("This rewriting only supports absolute URIs."); + } + int linkPort = link.getPort(); + if (linkPort == -1) { + linkPort = 80; + } + Service s; + try { + s = model.resolve(link.getHost(), linkPort, self); + } catch (IllegalArgumentException e) { + throw new GiveUpLinkRetargetingException(e); + } + newUri.append("/service/").append(s.getIdentifier(linkPort)); + newUri.append(link.getPath()); + } + +} diff --git a/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java b/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java new file mode 100644 index 00000000000..5cc9fa85775 --- /dev/null +++ b/configserver/src/main/java/com/yahoo/vespa/serviceview/package-info.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * Home of the centralised service view implementation. The service view is a + * REST API for discovering and accessing the state API for any service in a + * Vespa cluster. + * + * <p>Do note this package is in its prototyping stage and classes <i>will</i> + * be renamed and moved around a little.</p> + */ +@ExportPackage +package com.yahoo.vespa.serviceview; + +import com.yahoo.osgi.annotation.ExportPackage; |