com.yahoo.vespa.model.AbstractService Maven / Gradle / Ivy
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.model;
import com.yahoo.config.model.api.PortInfo;
import com.yahoo.config.model.api.ServiceInfo;
import com.yahoo.config.model.deploy.DeployState;
import com.yahoo.config.model.producer.AnyConfigProducer;
import com.yahoo.config.model.producer.TreeConfigProducer;
import com.yahoo.vespa.defaults.Defaults;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import static com.yahoo.text.Lowercase.toLowerCase;
/**
* Superclass for all Processes.
*
* @author gjoranv
*/
public abstract class AbstractService extends TreeConfigProducer implements Service {
// The physical host this Service runs on.
private HostResource hostResource = null;
/**
* Identifier that ensures that multiple instances of the same
* Service subclass will have unique names on the host. The first
* instance of one kind of Service will have the id 1, and the id
* will increase by 1 for each new instance.
* TODO: Do something more intelligent in Host?
*/
private int id = 0;
/** The actual base port for this Service. */
private int basePort = 0;
/** The ports allocated to this Service. */
private List ports = new ArrayList<>();
/** The optional JVM execution options for this Service. */
// Please keep non-null, as passed to command line in service startup
private String jvmOptions = "";
/** The optional PRELOAD libraries for this Service. */
// Please keep non-null, as passed to command line in service startup
private String preload = null;
private final Map environmentVariables = new TreeMap<>();
/** The ports metainfo object */
protected PortsMeta portsMeta = new PortsMeta();
/**
* Custom properties that a service may populate to communicate
* more key/value pairs to the service-list dump.
* Supported key datatypes are String, and values may be String or Integer.
*/
private final Map serviceProperties = new LinkedHashMap<>();
/** The affinity properties of this service. */
private Optional affinity = Optional.empty();
private boolean initialized = false;
protected String defaultPreload() {
return Defaults.getDefaults().underVespaHome("lib64/vespa/malloc/libvespamalloc.so");
}
/**
* Preferred constructor when building from XML. Use this if you are building
* in doBuild() in an TreeConfigProducerBuilder.
* build() will call initService() in that case, after setting hostalias and baseport.
*
* @param parent the parent config producer in the model tree
* @param name the name of this service
*/
public AbstractService(TreeConfigProducer> parent, String name) {
super(parent, name);
environmentVariables.put("VESPA_SILENCE_CORE_ON_OOM", true);
}
/**
* Only used for testing. Stay away.
*
* @param name the name of this service.
*/
public AbstractService(String name) {
this(null, name);
}
@Override
public void remove() {
super.remove();
if (hostResource != null)
hostResource.deallocateService(this);
}
/**
* Distribute affinity on a collection of services. Services that are located on the same host
* will be assigned a specific cpu socket on that host.
*
* @param services A {@link Collection} of services of the same type, not necessarily on the same host.
*/
public static void distributeCpuSocketAffinity(Collection services) {
Map> affinityMap = new HashMap<>();
for (SERVICE service : services) {
if (!affinityMap.containsKey(service.getHostResource())) {
affinityMap.put(service.getHostResource(), new ArrayList<>());
}
int cpuSocket = affinityMap.get(service.getHostResource()).size();
affinityMap.get(service.getHostResource()).add(service);
service.setAffinity(new Affinity.Builder().cpuSocket(cpuSocket).build());
}
}
/**
* Helper method to avoid replicating code.
*
* @param hostResource The physical host on which this service should run.
* @param userPort The wanted port given by the user.
*/
private void initService(DeployState deployState, HostResource hostResource, int userPort) {
if (initialized) {
throw new IllegalStateException("Service '" + getConfigId() + "' already initialized.");
}
if (hostResource == null) {
throw new IllegalArgumentException("No host found for service '" + getServiceName() + "'. " +
"The hostalias is probably missing from hosts.xml.");
}
id = getIndex(hostResource);
ports = hostResource.allocateService(deployState.getDeployLogger(), this, getInstanceWantedPort(userPort));
initialized = true;
for(String envVar : deployState.getProperties().environmentVariables()) {
addEnvironmentVariable(envVar);
}
}
/**
* Called by builder class which has not given the host or port in a constructor, hence
* initService is not yet run for this.
*/
public void initService(DeployState deployState) {
initService(deployState, this.hostResource, this.basePort);
}
/**
* Returns the desired base port for the first instance of the
* service type. Returns '0' as default, which means that the
* service type should use the default port allocation mechanism.
*
* @return The desired base port for the first instance of the service type.
*/
public int getWantedPort() {
return 0;
}
/**
* Returns the desired base port for this service instance, '0' if
* it should use the default port allocation mechanism.
*
* @param userWantedPort The wanted port given by the user.
* @return The desired base port for this service instance, '0' by default
*/
private int getInstanceWantedPort(int userWantedPort) {
int wantedPort = 0;
if (userWantedPort == 0) {
if (requiresWantedPort())
wantedPort = getWantedPort();
else if (getWantedPort() > 0)
wantedPort = getWantedPort() + ((getId() - 1) * getPortCount());
} else {
// User defined from spec
wantedPort = userWantedPort;
}
return wantedPort;
}
/**
* Override if the desired base port (returned by getWantedPort()) is the only allowed base port.
*
* @return false by default
*/
public boolean requiresWantedPort() {
return false;
}
/**
* Gets the ports metainfo object. The service implementation must populate this object in the constructor.
*/
public PortsMeta getPortsMeta() {
return portsMeta;
}
/**
* Computes and returns the i'th port for this service, based on this Service's baseport.
*
* @param i the offset from 'basePort' of the port to return
* @return the i'th port relative to the base port.
* @throws IllegalStateException if i is out of range.
*/
public int getRelativePort(int i) {
if (ports.isEmpty()) {
throw new IllegalStateException("Requested port with offset " + i + " for service that " +
"has not reserved any ports: " + this);
}
if (i >= ports.size()) {
throw new IllegalStateException("Requested port with offset " + i + " for service that " +
"only has reserved " + ports.size() + " ports: " + this);
}
return ports.get(i);
}
/**
* Must be overridden by services that should be started by
* config-sentinel. The returned value will be used in
* config-sentinel configuration. Returns empty by default.
*
* @return empty by default.
*/
public Optional getStartupCommand() { return Optional.empty(); }
public Optional getPreShutdownCommand() {
return Optional.empty();
}
/** Returns the name that identifies this service for the config-sentinel, never null */
@Override
public String getServiceName() {
return getServiceType() + ((id == 1) ? "" : Integer.toString(id));
}
/**
* Returns the type of service. This is the class name without the
* package prefix by default, never null
*/
@Override
public String getServiceType() {
return toLowerCase(getShortClassName());
}
/**
* Strips the package prefix and returns the short classname.
*
* @return classname without package prefix.
*/
private String getShortClassName() {
Class myClass = getClass();
Package myPackage = myClass.getPackage();
return myClass.getName().substring(1 + myPackage.getName().length());
}
@Override
public HostResource getHost() { return hostResource; }
@Override
public String getHostName() {
return hostResource.getHostname();
}
/**
* @return The id (index) of this service on the host where it runs
*/
public int getId() {
return id;
}
/**
* Computes a number that identifies the service on the given
* host. The number of services of the same type (Class) is
* counted and the number is returned.
*
* @param host the host on which the service will run
* @return id number for the given service.
*/
// TODO: Do something more intelligent in the Host class..?
protected int getIndex(HostResource host) {
int i = 0;
for (Service s : host.getServices()) {
if (s.getServiceType().equals(getServiceType()) && (s != this)) {
i++;
}
}
return i + 1;
}
@Override
public ServiceInfo getServiceInfo() {
Set portInfos = new LinkedHashSet<>();
for (int i = 0; i < portsMeta.getNumPorts(); i++) {
portInfos.add(new PortInfo(ports.get(i), new LinkedHashSet<>(portsMeta.getTagsAt(i))));
}
Map properties = new LinkedHashMap<>();
for (Map.Entry prop : serviceProperties.entrySet()) {
properties.put(prop.getKey(), prop.getValue().toString());
}
return new ServiceInfo(getServiceName(), getServiceType(), portInfos, properties, getConfigId(), getHostName());
}
/**
* Sets a service property value for the given key.
*
* @param key a key used for this property
* @param value a String value associated with the key
* @return this service
*/
public AbstractService setProp(String key, String value) {
serviceProperties.put(key, value);
return this;
}
/**
* Sets a service property value for the given key.
*
* @param key a key used for this property
* @param value an Integer value associated with the key
* @return this service
*/
public AbstractService setProp(String key, Integer value) {
serviceProperties.put(key, value);
return this;
}
/**
* Gets a service property value mapped to the given key
* as a String, or null if no such key exists.
*
* @param key a key used for lookup in the service properties
* @return the associated String value for the given key, or null
*/
public String getServicePropertyString(String key) {
return getServicePropertyString(key, null);
}
@Override
public String getServicePropertyString(String key, String defStr) {
Object result = serviceProperties.get(key);
return (result == null) ? defStr : result.toString();
}
/** Optional execution args for this service */
public String getJvmOptions() {
return jvmOptions;
}
public final void setJvmOptions(String args) {
jvmOptions = (args == null) ? "" : args;
}
public final void appendJvmOptions(String args) {
if ((args != null) && !args.isEmpty()) {
setJvmOptions(jvmOptions + getSeparator(jvmOptions) + args);
}
}
private static String getSeparator(String current) {
return ("".equals(current)) ? "" : " ";
}
public final void prependJvmOptions(String args) {
if ((args != null) && !args.isEmpty()) {
setJvmOptions(args + getSeparator(jvmOptions) + jvmOptions);
}
}
public String getPreLoad() {
return preload != null ? preload : defaultPreload();
}
public void setPreLoad(String preload) {
this.preload = preload;
}
/** If larger or equal to 0 it mean that explicit mmaps shall not be included in coredump.*/
public void setMMapNoCoreLimit(long noCoreLimit) {
if (noCoreLimit >= 0) {
environmentVariables.put("VESPA_MMAP_NOCORE_LIMIT", noCoreLimit);
} else {
environmentVariables.remove("VESPA_MMAP_NOCORE_LIMIT");
}
}
public void setCoreOnOOM(boolean coreOnOOM) {
if ( ! coreOnOOM) {
environmentVariables.put("VESPA_SILENCE_CORE_ON_OOM", true);
} else {
environmentVariables.remove("VESPA_SILENCE_CORE_ON_OOM");
}
}
public void setNoVespaMalloc(String s) { environmentVariables.put("VESPA_USE_NO_VESPAMALLOC", s); }
public void setVespaMalloc(String s) { environmentVariables.put("VESPA_USE_VESPAMALLOC", s); }
public void setVespaMallocDebug(String s) { environmentVariables.put("VESPA_USE_VESPAMALLOC_D", s); }
public void setVespaMallocDebugStackTrace(String s) { environmentVariables.put("VESPA_USE_VESPAMALLOC_DST", s); }
private static String toEnvValue(Object o) {
if (o instanceof Number || o instanceof Boolean) {
return o.toString();
}
return '"' + o.toString() + '"';
}
public void addEnvironmentVariable(String nameAndValue) {
int pos = nameAndValue.indexOf('=');
environmentVariables.put(nameAndValue.substring(0, pos), nameAndValue.substring(pos+1));
}
public void addEnvironmentVariable(String name, Object value) {
environmentVariables.put(name, value);
}
@Override
public Map getEnvVars() {
return Collections.unmodifiableMap(environmentVariables);
}
public String getEnvStringForTesting() {
return environmentVariables.entrySet().stream().sorted(Map.Entry.comparingByKey())
.map(e -> e.getKey() + '=' + toEnvValue(e.getValue())).collect(Collectors.joining(" "));
}
/**
* WARNING: should only be called before initService()
*/
public void setBasePort(int wantedPort) {
this.basePort = wantedPort;
}
public void setHostResource(HostResource hostResource) {
this.hostResource = hostResource;
}
public boolean isInitialized() {
return initialized;
}
/** The service HTTP port for health status */
public int getHealthPort() { return -1;}
/**
* Overridden by subclasses. List of default dimensions to be added to this services metrics
*
* @return the default dimensions for this service
*/
public HashMap getDefaultMetricDimensions(){ return new LinkedHashMap<>(); }
// For testing
public int getNumPortsAllocated() {
return ports.size();
}
public HostResource getHostResource() {
return hostResource;
}
public Optional getAffinity() {
return affinity;
}
public void setAffinity(Affinity affinity) {
this.affinity = Optional.ofNullable(affinity);
}
@Override
public String toString() {
return getServiceName() + " on " + (getHost() == null ? "no host" : getHost().toString());
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy