// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.model.container; import com.yahoo.config.model.api.ModelContext; import com.yahoo.config.model.api.container.ContainerServiceType; import com.yahoo.config.model.deploy.DeployState; import com.yahoo.config.model.producer.AnyConfigProducer; import com.yahoo.config.model.producer.TreeConfigProducer; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.container.ComponentsConfig; import com.yahoo.container.QrConfig; import com.yahoo.container.core.ContainerHttpConfig; import com.yahoo.container.jdisc.ContainerMbusConfig; import com.yahoo.container.jdisc.JdiscBindingsConfig; import com.yahoo.osgi.provider.model.ComponentModel; import com.yahoo.search.config.QrStartConfig; import com.yahoo.vespa.defaults.Defaults; import com.yahoo.vespa.model.AbstractService; import com.yahoo.vespa.model.LogctlSpec; import com.yahoo.vespa.model.PortAllocBridge; import com.yahoo.vespa.model.application.validation.RestartConfigs; import com.yahoo.vespa.model.container.component.Component; import com.yahoo.vespa.model.container.component.ComponentGroup; import com.yahoo.vespa.model.container.component.ComponentsConfigGenerator; import com.yahoo.vespa.model.container.component.DiscBindingsConfigGenerator; import com.yahoo.vespa.model.container.component.Handler; import com.yahoo.vespa.model.container.component.SimpleComponent; import com.yahoo.vespa.model.container.http.ConnectorFactory; import com.yahoo.vespa.model.container.http.Http; import com.yahoo.vespa.model.container.http.JettyHttpServer; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.Optional; import static com.yahoo.container.QrConfig.Rpc; import static com.yahoo.vespa.defaults.Defaults.getDefaults; /** * Note about components: In general, all components should belong to the cluster and not the container. However, * components that need node specific config must be added at the container level, along with the node-specific * parts of the config generation (getConfig). * * @author gjoranv * @author Einar M R Rosenvinge * @author Tony Vaagenes */ // qr is restart because it is handled by ConfiguredApplication.start @RestartConfigs({QrStartConfig.class, QrConfig.class}) public abstract class Container extends AbstractService implements QrConfig.Producer, ComponentsConfig.Producer, JdiscBindingsConfig.Producer, ContainerHttpConfig.Producer, ContainerMbusConfig.Producer { public static final int BASEPORT = Defaults.getDefaults().vespaWebServicePort(); public static final String SINGLENODE_CONTAINER_SERVICESPEC = "default_singlenode_container"; /** The cluster this container belongs to, or null if it is not added to any cluster */ private ContainerCluster owner = null; private List logctlSpecs = List.of(); protected final TreeConfigProducer parent; private final String name; private boolean requireSpecificPorts = true; private String clusterName = null; private Optional hostResponseHeaderKey = Optional.empty(); /** Whether this node has been marked as retired (e.g, will be removed) */ private final boolean retired; /** The unique index of this node */ private final int index; private final boolean dumpHeapOnShutdownTimeout; private final double shutdownTimeoutS; private final ComponentGroup handlers = new ComponentGroup<>(this, "handler"); private final ComponentGroup> components = new ComponentGroup<>(this, "components"); private final JettyHttpServer defaultHttpServer; protected Container(TreeConfigProducer parent, String name, int index, DeployState deployState) { this(parent, name, false, index, deployState); } protected Container(TreeConfigProducer parent, String name, boolean retired, int index, DeployState deployState) { super(parent, name); this.name = name; this.parent = parent; this.retired = retired; this.index = index; dumpHeapOnShutdownTimeout = deployState.featureFlags().containerDumpHeapOnShutdownTimeout(); shutdownTimeoutS = deployState.featureFlags().containerShutdownTimeout(); this.defaultHttpServer = new JettyHttpServer("DefaultHttpServer", containerClusterOrNull(parent), deployState); if (getHttp() == null) { addChild(defaultHttpServer); } addBuiltinHandlers(); addChild(new SimpleComponent("com.yahoo.container.jdisc.ConfiguredApplication$ApplicationContext")); appendJvmOptions(jvmOmitStackTraceInFastThrowOption(deployState.featureFlags())); addEnvironmentVariable("VESPA_MALLOC_MMAP_THRESHOLD","0x1000000"); // 16M } protected String jvmOmitStackTraceInFastThrowOption(ModelContext.FeatureFlags featureFlags) { return featureFlags.jvmOmitStackTraceInFastThrowOption(ClusterSpec.Type.container); } void setOwner(ContainerCluster owner) { this.owner = owner; } /** True if this container is retired (slated for removal) */ public boolean isRetired() { return retired; } public ComponentGroup getHandlers() { return handlers; } public ComponentGroup getComponents() { return components; } public final void addComponent(Component c) { components.addComponent(c); } public final void addSimpleComponent(String idSpec, String classSpec, String bundleSpec) { addComponent(new SimpleComponent(new ComponentModel(idSpec, classSpec, bundleSpec))); } public final void addHandler(Handler h) { handlers.addComponent(h); } /** * If present, this container should emit this header key with the value set to the local hostname * in HTTP responses */ @SuppressWarnings("unused") // used by amenders public void setHostResponseHeaderKey(Optional hostResponseheaderKey) { Objects.requireNonNull(hostResponseheaderKey, "HostResponseheaderKey cannot be null"); this.hostResponseHeaderKey = hostResponseheaderKey; } public Http getHttp() { return (parent instanceof ContainerCluster) ? ((ContainerCluster) parent).getHttp() : null; } @SuppressWarnings("unused") // used by amenders public JettyHttpServer getDefaultHttpServer() { return defaultHttpServer; } /** Returns the index of this node. The index of a given node is stable through changes with best effort. */ public final int index() { return index; } // We cannot set bindings yet, as baseport is not initialized public void addBuiltinHandlers() { } @Override public void initService(DeployState deployState) { if (isInitialized()) return; // XXX: Must be called first, to set the baseport super.initService(deployState); if (getHttp() == null) { initDefaultJettyConnector(); } } private int getPort(ConnectorFactory connectorFactory) { return connectorFactory.getListenPort(); } private void initDefaultJettyConnector() { defaultHttpServer.addConnector(new ConnectorFactory.Builder("SearchServer", getSearchPort()).build()); } private ContainerServiceType myServiceType = null; /** Subclasses must implement {@link #myServiceType()} for a custom service name. */ @Override public final String getServiceType() { if (myServiceType == null) { myServiceType = myServiceType(); } return myServiceType.serviceName; } /** Subclasses must implement this for a custom service name. */ protected abstract ContainerServiceType myServiceType(); public void setClusterName(String name) { this.clusterName = name; } @Override public int getWantedPort() { return requiresWantedPort() ? BASEPORT: 0; } /** instance can use any port number for its default HTTP server */ public void useDynamicPorts() { requireSpecificPorts = false; } /** * First container must run on ports familiar to the user. */ @Override public boolean requiresWantedPort() { return requireSpecificPorts && (getHttp() == null); } /** * @return the number of ports needed by the Container */ public int getPortCount() { int httpPorts = (getHttp() != null) ? 0 : 2; return httpPorts + numMessageBusPorts() + numRpcPorts(); } @Override public void allocatePorts(int start, PortAllocBridge from) { if (start == 0) start = BASEPORT; int offset = 0; if (getHttp() == null) { if (requireSpecificPorts) { allocatedSearchPort = from.requirePort(start, "http"); } else { allocatedSearchPort = from.allocatePort("http"); } portsMeta.on(offset++).tag("http").tag("query").tag("external").tag("state"); // XXX unused - remove: from.allocatePort("http/1"); portsMeta.on(offset++).tag("http").tag("external"); } else if (getHttp().getHttpServer().isEmpty()) { // no http server ports } else { for (ConnectorFactory connectorFactory : getHttp().getHttpServer().get().getConnectorFactories()) { int port = getPort(connectorFactory); String name = "http/" + connectorFactory.getName(); from.requirePort(port, name); if (offset == 0) { portsMeta.on(offset++).tag("http").tag("query").tag("external").tag("state"); } else { portsMeta.on(offset++).tag("http").tag("external"); } } } if (messageBusEnabled()) { allocatedMessagingPort = from.allocatePort("messaging"); portsMeta.on(offset++).tag("rpc").tag("messaging"); } if (rpcServerEnabled()) { allocatedRpcPort = from.allocatePort("rpc/admin"); portsMeta.on(offset++).tag("rpc").tag("admin"); } } protected int allocatedSearchPort = 0; /** * @return the actual search port * TODO: Remove. Use {@link #getPortsMeta()} and check tags in conjunction with {@link #getRelativePort(int)}. */ public int getSearchPort() { if (getHttp() != null) throw new AssertionError("getSearchPort must not be used when http section is present."); return allocatedSearchPort; } protected int allocatedRpcPort = 0; protected int getRpcPort() { return allocatedRpcPort; } protected int numRpcPorts() { return rpcServerEnabled() ? 1 : 0; } protected int allocatedMessagingPort = 0; private int getMessagingPort() { return allocatedMessagingPort; } protected int numMessageBusPorts() { return messageBusEnabled() ? 1 : 0; } @Override public int getHealthPort() { final Http http = getHttp(); if (http != null) { // TODO: allow the user to specify health port manually if (http.getHttpServer().isEmpty()) { return -1; } else { return getRelativePort(0); } } else { return httpServerEnabled() ? getSearchPort() : -1; } } public Optional getStartupCommand() { return Optional.of("PRELOAD=" + getPreLoad() + " exec ${VESPA_HOME}/libexec/vespa/vespa-wrapper vespa-start-container-daemon " + getJvmOptions() + " "); } @Override public void getConfig(QrConfig.Builder builder) { builder.rpc(new Rpc.Builder() .enabled(rpcServerEnabled()) .port(getRpcPort()) .slobrokId(serviceSlobrokId())) .discriminator((clusterName != null ? clusterName + "." : "" ) + name) .clustername(clusterName != null ? clusterName : "") .nodeIndex(index) .shutdown.dumpHeapOnTimeout(dumpHeapOnShutdownTimeout) .timeout(shutdownTimeoutS); } /** Returns the jvm args set explicitly for this node */ public String getAssignedJvmOptions() { return super.getJvmOptions(); } private String serviceSlobrokId() { return "vespa/service/" + getConfigId(); } @Override public void getConfig(ComponentsConfig.Builder builder) { builder.setApplyOnRestart(owner.getDeferChangesUntilRestart()); // Sufficient to set on one config builder.components.addAll(ComponentsConfigGenerator.generate(allEnabledComponents())); } private Collection> allEnabledComponents() { Collection> allComponents = new ArrayList<>(); addAllEnabledComponents(allComponents, this); return Collections.unmodifiableCollection(allComponents); } private void addAllEnabledComponents(Collection> allComponents, TreeConfigProducer current) { for (var child: current.getChildren().values()) { if ( ! httpServerEnabled() && isHttpServer(child)) continue; if (child instanceof Component) allComponents.add((Component) child); if (child instanceof TreeConfigProducer t) { addAllEnabledComponents(allComponents, t); } } } private boolean isHttpServer(AnyConfigProducer component) { return component instanceof JettyHttpServer; } @Override public final void getConfig(JdiscBindingsConfig.Builder builder) { builder.handlers(DiscBindingsConfigGenerator.generate(handlers.getComponents())); } @Override public void getConfig(ContainerHttpConfig.Builder builder) { hostResponseHeaderKey.ifPresent(builder::hostResponseHeaderKey); } @Override public void getConfig(ContainerMbusConfig.Builder builder) { builder.port(getMessagingPort()); } @Override public HashMap getDefaultMetricDimensions(){ HashMap dimensions = new HashMap<>(); if (clusterName != null) dimensions.put("clustername", clusterName); return dimensions; } protected String prepareStopCommand(Duration timeout) { long rpcTimeoutSeconds = timeout.toSeconds() + 10; String rpcParams = "-t " + rpcTimeoutSeconds + " tcp/localhost:" + getRpcPort() + " prepareStop d:" + timeout.toSeconds(); return getDefaults().underVespaHome("bin/vespa-rpc-invoke") + " " + rpcParams; } private boolean messageBusEnabled() { return containerCluster().isPresent() && containerCluster().get().messageBusEnabled(); } private boolean httpServerEnabled() { return containerCluster().isPresent() && containerCluster().get().httpServerEnabled(); } private boolean rpcServerEnabled() { return containerCluster().isPresent() && containerCluster().get().rpcServerEnabled(); } protected Optional containerCluster() { return Optional.ofNullable(containerClusterOrNull(parent)); } private static ContainerCluster containerClusterOrNull(AnyConfigProducer producer) { return producer instanceof ContainerCluster ? (ContainerCluster) producer : null; } void setLogctlSpecs(List logctlSpecs) { this.logctlSpecs = logctlSpecs; } @Override public List getLogctlSpecs() { return logctlSpecs; } }