diff options
75 files changed, 2634 insertions, 737 deletions
diff --git a/config-model/src/main/java/com/yahoo/vespa/model/Host.java b/config-model/src/main/java/com/yahoo/vespa/model/Host.java index 0adfe9e4bdb..624a9fd4da7 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/Host.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/Host.java @@ -42,16 +42,14 @@ public final class Host extends AbstractConfigProducer<AbstractConfigProducer<?> private void checkName(HostSystem parent, String hostname) { // Give a warning if the host does not exist - // Host exists - warn if given hostname is not a fully qualified one. - String canonical = hostname; try { - canonical = parent.getCanonicalHostname(hostname); + Object address = java.net.InetAddress.getByName(hostname); } catch (UnknownHostException e) { - deployLogger().log(Level.WARNING, "Unable to find canonical hostname of host: " + hostname); + deployLogger().log(Level.WARNING, "Unable to lookup IP address of host: " + hostname); } - if ((null != canonical) && (! hostname.equals(canonical))) { + if (! hostname.contains(".")) { deployLogger().log(Level.WARNING, "Host named '" + hostname + "' may not receive any config " + - "since it does not match its canonical hostname: " + canonical); + "since it is not a canonical hostname"); } } diff --git a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java index 6467199d9f9..fc46ed18dde 100644 --- a/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java +++ b/config-model/src/main/java/com/yahoo/vespa/model/admin/monitoring/VespaMetricSet.java @@ -110,6 +110,13 @@ public class VespaMetricSet { metrics.add(new Metric("jdisc.memory_mappings.max")); metrics.add(new Metric("jdisc.open_file_descriptors.max")); + metrics.add(new Metric("jdisc.gc.count.average")); + metrics.add(new Metric("jdisc.gc.count.max")); + metrics.add(new Metric("jdisc.gc.count.last")); + metrics.add(new Metric("jdisc.gc.ms.average")); + metrics.add(new Metric("jdisc.gc.ms.max")); + metrics.add(new Metric("jdisc.gc.ms.last")); + metrics.add(new Metric("jdisc.deactivated_containers.total.last")); metrics.add(new Metric("jdisc.deactivated_containers.with_retained_refs.last")); diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java index c6a390caf86..2a53f9ee45c 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/ConfigServerMaintenance.java @@ -51,7 +51,7 @@ public class ConfigServerMaintenance extends AbstractComponent { this.defaultInterval = Duration.ofMinutes(configserverConfig.maintainerIntervalMinutes()); // TODO: Want job control or feature flag to control when to run this, for now use a very // long interval to avoid running the maintainer - this.tenantsMaintainerInterval = isCd || isTest + this.tenantsMaintainerInterval = isCd || isTest || configserverConfig.region().equals("us-central-1") ? defaultInterval : Duration.ofMinutes(configserverConfig.tenantsMaintainerIntervalMinutes()); } diff --git a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java index 2664a0bde8c..1d16283d938 100644 --- a/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java +++ b/configserver/src/main/java/com/yahoo/vespa/config/server/maintenance/FileDistributionMaintainer.java @@ -31,9 +31,10 @@ public class FileDistributionMaintainer extends Maintainer { @Override protected void maintain() { - // TODO: For now only deletes files in CD system + // TODO: Delete files in all zones boolean deleteFiles = (SystemName.from(configserverConfig.system()) == SystemName.cd) - || Environment.from(configserverConfig.environment()).isTest(); + || Environment.from(configserverConfig.environment()).isTest() + || configserverConfig.region().equals("us-central-1"); applicationRepository.deleteUnusedFiledistributionReferences(fileReferencesDir, deleteFiles); } } diff --git a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java index c0c3683bebb..992d46d3115 100644 --- a/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java +++ b/configserver/src/test/java/com/yahoo/vespa/config/server/ConfigServerBootstrapTest.java @@ -47,9 +47,9 @@ public class ConfigServerBootstrapTest { VipStatus vipStatus = new VipStatus(); ConfigServerBootstrap bootstrap = new ConfigServerBootstrap(tester.applicationRepository(), rpcServer, versionState, createStateMonitor(), vipStatus); assertFalse(vipStatus.isInRotation()); - waitUntil(() -> bootstrap.status() == StateMonitor.Status.up, "failed waiting for status 'up'"); waitUntil(rpcServer::isRunning, "failed waiting for Rpc server running"); - assertTrue(vipStatus.isInRotation()); + waitUntil(() -> bootstrap.status() == StateMonitor.Status.up, "failed waiting for status 'up'"); + waitUntil(vipStatus::isInRotation, "failed waiting for server to be in rotation"); bootstrap.deconstruct(); assertEquals(StateMonitor.Status.down, bootstrap.status()); diff --git a/container-dependency-versions/pom.xml b/container-dependency-versions/pom.xml index b4af6800768..f546a4e36d2 100644 --- a/container-dependency-versions/pom.xml +++ b/container-dependency-versions/pom.xml @@ -459,7 +459,7 @@ <properties> <bouncycastle.version>1.58</bouncycastle.version> - <felix.version>5.0.1</felix.version> + <felix.version>5.4.0</felix.version> <findbugs.version>1.3.9</findbugs.version> <guava.version>18.0</guava.version> <guice.version>3.0</guice.version> diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetrics.java b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetrics.java new file mode 100644 index 00000000000..04fd8572ad4 --- /dev/null +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetrics.java @@ -0,0 +1,94 @@ +package com.yahoo.container.jdisc.metric; + +import com.yahoo.jdisc.Metric; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +/** + * @author ollivir + */ +public class GarbageCollectionMetrics { + private static final String GC_COUNT = "jdisc.gc.count"; + private static final String GC_TIME = "jdisc.gc.ms"; + private static final String DIMENSION_KEY = "gcName"; + + public static final Duration REPORTING_INTERVAL = Duration.ofSeconds(62); + + static class GcStats { + private final Instant when; + private final long count; + private final Duration totalRuntime; + + private GcStats(Instant when, long count, Duration totalRuntime) { + this.when = when; + this.count = count; + this.totalRuntime = totalRuntime; + } + } + + private Map<String, LinkedList<GcStats>> gcStatistics; + + private final Clock clock; + + public GarbageCollectionMetrics(Clock clock) { + this.clock = clock; + this.gcStatistics = new HashMap<>(); + collectGcStatistics(clock.instant()); + } + + private void collectGcStatistics(Instant now) { + for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) { + String gcName = gcBean.getName().replace(" ", ""); + GcStats stats = new GcStats(now, gcBean.getCollectionCount(), Duration.ofMillis(gcBean.getCollectionTime())); + + LinkedList<GcStats> window = gcStatistics.computeIfAbsent(gcName, anyName -> new LinkedList<>()); + window.addLast(stats); + } + } + + private void cleanStatistics(Instant now) { + Instant oldestToKeep = now.minus(REPORTING_INTERVAL); + + for(Iterator<Map.Entry<String, LinkedList<GcStats>>> it = gcStatistics.entrySet().iterator(); it.hasNext(); ) { + Map.Entry<String, LinkedList<GcStats>> entry = it.next(); + LinkedList<GcStats> history = entry.getValue(); + while(history.isEmpty() == false && oldestToKeep.isAfter(history.getFirst().when)) { + history.removeFirst(); + } + if(history.isEmpty()) { + it.remove(); + } + } + } + + public void emitMetrics(Metric metric) { + Instant now = clock.instant(); + + collectGcStatistics(now); + cleanStatistics(now); + + for (Map.Entry<String, LinkedList<GcStats>> item : gcStatistics.entrySet()) { + GcStats reference = item.getValue().getFirst(); + GcStats latest = item.getValue().getLast(); + Map<String, String> contextData = new HashMap<>(); + contextData.put(DIMENSION_KEY, item.getKey()); + Metric.Context gcContext = metric.createContext(contextData); + + metric.set(GC_COUNT, latest.count - reference.count, gcContext); + metric.set(GC_TIME, latest.totalRuntime.minus(reference.totalRuntime).toMillis(), gcContext); + } + } + + // partial exposure for testing + Map<String, LinkedList<GcStats>> getGcStatistics() { + return gcStatistics; + } +} diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java index 22b049c9ab7..c2ef789e8fc 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/metric/MetricUpdater.java @@ -10,6 +10,7 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Clock; import java.time.Duration; import java.util.Timer; import java.util.TimerTask; @@ -89,10 +90,12 @@ public class MetricUpdater extends AbstractComponent { private final Runtime runtime = Runtime.getRuntime(); private final Metric metric; private final ContainerWatchdogMetrics containerWatchdogMetrics; + private final GarbageCollectionMetrics garbageCollectionMetrics; public UpdaterTask(Metric metric, ContainerWatchdogMetrics containerWatchdogMetrics) { this.metric = metric; this.containerWatchdogMetrics = containerWatchdogMetrics; + this.garbageCollectionMetrics = new GarbageCollectionMetrics(Clock.systemUTC()); } @SuppressWarnings("deprecation") @@ -109,9 +112,10 @@ public class MetricUpdater extends AbstractComponent { metric.set(TOTAL_MEMORY_BYTES, totalMemory, null); metric.set(MEMORY_MAPPINGS_COUNT, count_mappings(), null); metric.set(OPEN_FILE_DESCRIPTORS, count_open_files(), null); + containerWatchdogMetrics.emitMetrics(metric); + garbageCollectionMetrics.emitMetrics(metric); } - } private static class TimerScheduler implements Scheduler { diff --git a/container-disc/src/test/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetricsTest.java b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetricsTest.java new file mode 100644 index 00000000000..61d8763b852 --- /dev/null +++ b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/GarbageCollectionMetricsTest.java @@ -0,0 +1,57 @@ +package com.yahoo.container.jdisc.metric; + +import com.yahoo.jdisc.Metric; +import com.yahoo.test.ManualClock; +import org.junit.Test; + +import java.lang.management.ManagementFactory; +import java.time.Duration; +import java.util.LinkedList; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author ollivir + */ +public class GarbageCollectionMetricsTest { + @Test + public void gc_metrics_are_collected_in_a_sliding_window() { + ManualClock clock = new ManualClock(); + Metric metric = mock(Metric.class); + int garbageCollectors = ManagementFactory.getGarbageCollectorMXBeans().size(); + + Duration interval = GarbageCollectionMetrics.REPORTING_INTERVAL; + GarbageCollectionMetrics garbageCollectionMetrics = new GarbageCollectionMetrics(clock); + assertThat(garbageCollectionMetrics.getGcStatistics().keySet().size(), is(garbageCollectors)); + + clock.advance(interval.minus(Duration.ofMillis(10))); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 2); + + clock.advance(Duration.ofMillis(10)); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 3); + + clock.advance(Duration.ofMillis(10)); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 3); + + clock.advance(interval); + garbageCollectionMetrics.emitMetrics(metric); + assertWindowLengths(garbageCollectionMetrics, 2); + + verify(metric, times(garbageCollectors * 4 * 2)).set(anyString(), any(), any()); + } + + private static void assertWindowLengths(GarbageCollectionMetrics gcm, int count) { + for(LinkedList<GarbageCollectionMetrics.GcStats> window: gcm.getGcStatistics().values()) { + assertThat(window.size(), is(count)); + } + } +} diff --git a/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java index f10af7593a4..e9e04eab3b4 100644 --- a/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java +++ b/container-disc/src/test/java/com/yahoo/container/jdisc/metric/MetricUpdaterTest.java @@ -5,6 +5,7 @@ import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.statistics.ContainerWatchdogMetrics; import org.junit.Test; +import java.lang.management.ManagementFactory; import java.time.Duration; import static org.mockito.Matchers.any; @@ -20,11 +21,13 @@ public class MetricUpdaterTest { @Test public void metrics_are_updated_in_scheduler_cycle() throws InterruptedException { + int gcCount = ManagementFactory.getGarbageCollectorMXBeans().size(); + Metric metric = mock(Metric.class); ContainerWatchdogMetrics containerWatchdogMetrics = mock(ContainerWatchdogMetrics.class); new MetricUpdater(new MockScheduler(), metric, containerWatchdogMetrics); verify(containerWatchdogMetrics, times(1)).emitMetrics(any()); - verify(metric, times(8)).set(anyString(), any(), any()); + verify(metric, times(8 + 2 * gcCount)).set(anyString(), any(), any()); } private static class MockScheduler implements MetricUpdater.Scheduler { diff --git a/container-jersey2/pom.xml b/container-jersey2/pom.xml index 76ff21dc028..c5ed7d872bf 100644 --- a/container-jersey2/pom.xml +++ b/container-jersey2/pom.xml @@ -53,10 +53,6 @@ <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> </dependency> - <dependency> - <groupId>org.scala-lang</groupId> - <artifactId>scala-library</artifactId> - </dependency> </dependencies> <build> <plugins> diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java new file mode 100644 index 00000000000..7ff9646cb27 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ComponentGraphProvider.java @@ -0,0 +1,73 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import com.yahoo.container.di.config.ResolveDependencyException; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.jaxrs.annotation.Component; +import org.glassfish.hk2.api.Injectee; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceHandle; + +import javax.inject.Singleton; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Resolves jdisc container components for jersey 2 components. + * + * @author Tony Vaagenes + * @author ollivir + */ +@Singleton // jersey2 requirement: InjectionResolvers must be in the Singleton scope +public class ComponentGraphProvider implements InjectionResolver<Component> { + private Collection<RestApiContext.Injectable> injectables; + + public ComponentGraphProvider(Collection<RestApiContext.Injectable> injectables) { + this.injectables = injectables; + } + + @Override + public Object resolve(Injectee injectee, ServiceHandle<?> root) { + Class<?> wantedClass; + Type type = injectee.getRequiredType(); + if (type instanceof Class) { + wantedClass = (Class<?>) type; + } else { + throw new UnsupportedOperationException("Only classes are supported, got " + type); + } + + List<RestApiContext.Injectable> componentsWithMatchingType = new ArrayList<>(); + for (RestApiContext.Injectable injectable : injectables) { + if (wantedClass.isInstance(injectable.instance)) { + componentsWithMatchingType.add(injectable); + } + } + + if (componentsWithMatchingType.size() == 1) { + return componentsWithMatchingType.get(0).instance; + } else { + String injectionDescription = "class '" + wantedClass + "' to inject into Jersey resource/provider '" + + injectee.getInjecteeClass() + "')"; + if (componentsWithMatchingType.size() > 1) { + String ids = componentsWithMatchingType.stream().map(c -> c.id.toString()).collect(Collectors.joining(",")); + throw new ResolveDependencyException("Multiple components found of " + injectionDescription + ": " + ids); + } else { + throw new ResolveDependencyException("Could not find a component of " + injectionDescription + "."); + } + } + } + + @Override + public boolean isMethodParameterIndicator() { + return true; + } + + @Override + public boolean isConstructorParameterIndicator() { + return true; + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java new file mode 100644 index 00000000000..4c4e43bc8d5 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyApplication.java @@ -0,0 +1,25 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import javax.ws.rs.core.Application; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class JerseyApplication extends Application { + private Set<Class<?>> classes; + + public JerseyApplication(Collection<Class<?>> resourcesAndProviderClasses) { + this.classes = new HashSet<>(resourcesAndProviderClasses); + } + + @Override + public Set<Class<?>> getClasses() { + return classes; + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java new file mode 100644 index 00000000000..1dbe410ba54 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/JerseyServletProvider.java @@ -0,0 +1,118 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; +import com.yahoo.container.di.componentgraph.Provider; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.di.config.RestApiContext.BundleInfo; +import com.yahoo.container.jaxrs.annotation.Component; +import org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.Binder; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; +import org.objectweb.asm.ClassReader; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static com.yahoo.container.servlet.jersey.util.ResourceConfigUtil.registerComponent; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class JerseyServletProvider implements Provider<ServletHolder> { + private final ServletHolder jerseyServletHolder; + + public JerseyServletProvider(RestApiContext restApiContext) { + this.jerseyServletHolder = new ServletHolder(new ServletContainer(resourceConfig(restApiContext))); + } + + private ResourceConfig resourceConfig(RestApiContext restApiContext) { + final ResourceConfig resourceConfig = ResourceConfig + .forApplication(new JerseyApplication(resourcesAndProviders(restApiContext.getBundles()))); + + registerComponent(resourceConfig, componentInjectorBinder(restApiContext)); + registerComponent(resourceConfig, jacksonDatatypeJdk8Provider()); + resourceConfig.register(MultiPartFeature.class); + + return resourceConfig; + } + + private static Collection<Class<?>> resourcesAndProviders(Collection<BundleInfo> bundles) { + final List<Class<?>> ret = new ArrayList<>(); + + for (BundleInfo bundle : bundles) { + for (String classEntry : bundle.getClassEntries()) { + Optional<String> className = detectResourceOrProvider(bundle.classLoader, classEntry); + className.ifPresent(cname -> ret.add(loadClass(bundle.symbolicName, bundle.classLoader, cname))); + } + } + return ret; + } + + private static Optional<String> detectResourceOrProvider(ClassLoader bundleClassLoader, String classEntry) { + try (InputStream inputStream = getResourceAsStream(bundleClassLoader, classEntry)) { + ResourceOrProviderClassVisitor visitor = ResourceOrProviderClassVisitor.visit(new ClassReader(inputStream)); + return visitor.getJerseyClassName(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static InputStream getResourceAsStream(ClassLoader bundleClassLoader, String classEntry) { + InputStream is = bundleClassLoader.getResourceAsStream(classEntry); + if (is == null) { + throw new RuntimeException("No entry " + classEntry + " in bundle " + bundleClassLoader); + } else { + return is; + } + } + + private static Class<?> loadClass(String bundleSymbolicName, ClassLoader classLoader, String className) { + try { + return classLoader.loadClass(className); + } catch (Exception e) { + throw new RuntimeException("Failed loading class " + className + " from bundle " + bundleSymbolicName, e); + } + } + + private static Binder componentInjectorBinder(RestApiContext restApiContext) { + final ComponentGraphProvider componentGraphProvider = new ComponentGraphProvider(restApiContext.getInjectableComponents()); + final TypeLiteral<InjectionResolver<Component>> componentAnnotationType = new TypeLiteral<InjectionResolver<Component>>() { + }; + + return new AbstractBinder() { + @Override + public void configure() { + bind(componentGraphProvider).to(componentAnnotationType); + } + }; + } + + private static JacksonJaxbJsonProvider jacksonDatatypeJdk8Provider() { + JacksonJaxbJsonProvider provider = new JacksonJaxbJsonProvider(); + provider.setMapper(new ObjectMapper().registerModule(new Jdk8Module()).registerModule(new JavaTimeModule())); + return provider; + } + + @Override + public ServletHolder get() { + return jerseyServletHolder; + } + + @Override + public void deconstruct() { + } +} diff --git a/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java new file mode 100644 index 00000000000..7cb47ac6118 --- /dev/null +++ b/container-jersey2/src/main/java/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.java @@ -0,0 +1,103 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.servlet.jersey; + +import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; + +import javax.ws.rs.Path; +import javax.ws.rs.ext.Provider; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author ollivir + */ +public class ResourceOrProviderClassVisitor extends ClassVisitor { + private String className = null; + private boolean isPublic = false; + private boolean isAbstract = false; + + private boolean isInnerClass = false; + private boolean isStatic = false; + + private boolean isAnnotated = false; + + public ResourceOrProviderClassVisitor() { + super(Opcodes.ASM6); + } + + public Optional<String> getJerseyClassName() { + if (isJerseyClass()) { + return Optional.of(getClassName()); + } else { + return Optional.empty(); + } + } + + public boolean isJerseyClass() { + return isAnnotated && isPublic && !isAbstract && (!isInnerClass || isStatic); + } + + public String getClassName() { + assert (className != null); + return org.objectweb.asm.Type.getObjectType(className).getClassName(); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + isPublic = isPublic(access); + className = name; + isAbstract = isAbstract(access); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + assert (className != null); + + if (name.equals(className)) { + isInnerClass = true; + isStatic = isStatic(access); + } + } + + @Override + public AnnotationVisitor visitAnnotation(String desc, boolean visible) { + isAnnotated |= annotationClassDescriptors.contains(desc); + return null; + } + + private static Set<String> annotationClassDescriptors = new HashSet<>(); + + static { + annotationClassDescriptors.add(Type.getDescriptor(Path.class)); + annotationClassDescriptors.add(Type.getDescriptor(Provider.class)); + } + + private static boolean isPublic(int access) { + return isSet(Opcodes.ACC_PUBLIC, access); + } + + private static boolean isStatic(int access) { + return isSet(Opcodes.ACC_STATIC, access); + } + + private static boolean isAbstract(int access) { + return isSet(Opcodes.ACC_ABSTRACT, access); + } + + private static boolean isSet(int bits, int access) { + return (access & bits) == bits; + } + + public static ResourceOrProviderClassVisitor visit(ClassReader classReader) { + ResourceOrProviderClassVisitor visitor = new ResourceOrProviderClassVisitor(); + classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); + return visitor; + } +} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala deleted file mode 100644 index cabde3680a4..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ComponentGraphProvider.scala +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import javax.inject.Singleton - -import com.yahoo.container.di.config.{ResolveDependencyException, RestApiContext} -import com.yahoo.container.jaxrs.annotation.Component -import org.glassfish.hk2.api.{ServiceHandle, Injectee, InjectionResolver} - -/** - * Resolves jdisc container components for jersey 2 components. - * Similar to Gjoran's ComponentGraphProvider for jersey 1. - * @author tonytv - */ -@Singleton //jersey2 requirement: InjectionResolvers must be in the Singleton scope -class ComponentGraphProvider(injectables: Traversable[RestApiContext.Injectable]) extends InjectionResolver[Component] { - override def resolve(injectee: Injectee, root: ServiceHandle[_]): AnyRef = { - val wantedClass = injectee.getRequiredType match { - case c: Class[_] => c - case unsupported => throw new UnsupportedOperationException("Only classes are supported, got " + unsupported) - } - - val componentsWithMatchingType = injectables.filter{ injectable => - wantedClass.isInstance(injectable.instance) } - - val injectionDescription = - s"class '$wantedClass' to inject into Jersey resource/provider '${injectee.getInjecteeClass}')" - - if (componentsWithMatchingType.size > 1) - throw new ResolveDependencyException(s"Multiple components found of $injectionDescription: " + - componentsWithMatchingType.map(_.id).mkString(",")) - - componentsWithMatchingType.headOption.map(_.instance).getOrElse { - throw new ResolveDependencyException(s"Could not find a component of $injectionDescription.") - } - } - - override def isMethodParameterIndicator: Boolean = true - override def isConstructorParameterIndicator: Boolean = true -} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala deleted file mode 100644 index eea41003984..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyApplication.scala +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import javax.ws.rs.core.Application - -import scala.collection.JavaConverters._ - -/** - * @author tonytv - */ -class JerseyApplication(resourcesAndProviderClasses: Set[Class[_]]) extends Application { - private val classes: java.util.Set[Class[_]] = resourcesAndProviderClasses.asJava - - override def getClasses = classes - override def getSingletons = super.getSingletons -} diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala deleted file mode 100644 index f0eff54dc16..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/JerseyServletProvider.scala +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import java.io.{IOException, InputStream} - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.datatype.jdk8.Jdk8Module -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule -import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider -import com.yahoo.container.di.componentgraph.Provider -import com.yahoo.container.di.config.RestApiContext -import com.yahoo.container.di.config.RestApiContext.BundleInfo -import com.yahoo.container.jaxrs.annotation.Component -import com.yahoo.container.servlet.jersey.util.ResourceConfigUtil.registerComponent -import org.eclipse.jetty.servlet.ServletHolder -import org.glassfish.hk2.api.{InjectionResolver, TypeLiteral} -import org.glassfish.hk2.utilities.Binder -import org.glassfish.hk2.utilities.binding.AbstractBinder -import org.glassfish.jersey.media.multipart.MultiPartFeature -import org.glassfish.jersey.server.ResourceConfig -import org.glassfish.jersey.servlet.ServletContainer -import org.objectweb.asm.ClassReader - -import scala.collection.JavaConverters._ -import scala.util.control.Exception - - -/** - * @author tonytv - */ -class JerseyServletProvider(restApiContext: RestApiContext) extends Provider[ServletHolder] { - private val jerseyServletHolder = new ServletHolder(new ServletContainer(resourceConfig(restApiContext))) - - private def resourceConfig(restApiContext: RestApiContext) = { - val resourceConfig = ResourceConfig.forApplication( - new JerseyApplication(resourcesAndProviders(restApiContext.getBundles.asScala))) - - registerComponent(resourceConfig, componentInjectorBinder(restApiContext)) - registerComponent(resourceConfig, jacksonDatatypeJdk8Provider) - resourceConfig.register(classOf[MultiPartFeature]) - - resourceConfig - } - - def resourcesAndProviders(bundles: Traversable[BundleInfo]) = - (for { - bundle <- bundles.view - classEntry <- bundle.getClassEntries.asScala - className <- detectResourceOrProvider(bundle.classLoader, classEntry) - } yield loadClass(bundle.symbolicName, bundle.classLoader, className)).toSet - - - def detectResourceOrProvider(bundleClassLoader: ClassLoader, classEntry: String): Option[String] = { - using(getResourceAsStream(bundleClassLoader, classEntry)) { inputStream => - val visitor = ResourceOrProviderClassVisitor.visit(new ClassReader(inputStream)) - visitor.getJerseyClassName - } - } - - private def getResourceAsStream(bundleClassLoader: ClassLoader, classEntry: String) = { - bundleClassLoader.getResourceAsStream(classEntry) match { - case null => throw new RuntimeException(s"No entry $classEntry in bundle $bundleClassLoader") - case stream => stream - } - - } - - def using[T <: InputStream, R](stream: T)(f: T => R): R = { - try { - f(stream) - } finally { - Exception.ignoring(classOf[IOException]) { - stream.close() - } - } - } - - def loadClass(bundleSymbolicName: String, classLoader: ClassLoader, className: String) = { - try { - classLoader.loadClass(className) - } catch { - case e: Exception => throw new RuntimeException(s"Failed loading class $className from bundle $bundleSymbolicName", e) - } - } - - def componentInjectorBinder(restApiContext: RestApiContext): Binder = { - val componentGraphProvider = new ComponentGraphProvider(restApiContext.getInjectableComponents.asScala) - val componentAnnotationType = new TypeLiteral[InjectionResolver[Component]] {} - - new AbstractBinder { - override def configure() { - bind(componentGraphProvider).to(componentAnnotationType) - } - } - } - - def jacksonDatatypeJdk8Provider: JacksonJaxbJsonProvider = { - val provider = new JacksonJaxbJsonProvider() - provider.setMapper( - new ObjectMapper() - .registerModule(new Jdk8Module) - .registerModule(new JavaTimeModule)) - provider - } - - override def get() = jerseyServletHolder - override def deconstruct() {} -} - diff --git a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala b/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala deleted file mode 100644 index 52674026c25..00000000000 --- a/container-jersey2/src/main/scala/com/yahoo/container/servlet/jersey/ResourceOrProviderClassVisitor.scala +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.container.servlet.jersey - -import javax.ws.rs.Path -import javax.ws.rs.ext.Provider - -import org.objectweb.asm.{ClassVisitor, Opcodes, Type, AnnotationVisitor, ClassReader} - - -/** - * @author tonytv - */ -class ResourceOrProviderClassVisitor private () extends ClassVisitor(Opcodes.ASM6) { - private var className: String = null - private var isPublic: Boolean = false - private var isAbstract = false - - private var isInnerClass: Boolean = false - private var isStatic: Boolean = false - - private var isAnnotated: Boolean = false - - def getJerseyClassName: Option[String] = { - if (isJerseyClass) Some(getClassName) - else None - } - - def isJerseyClass: Boolean = { - isAnnotated && isPublic && !isAbstract && - (!isInnerClass || isStatic) - } - - def getClassName = { - assert (className != null) - Type.getObjectType(className).getClassName - } - - override def visit(version: Int, access: Int, name: String, signature: String, superName: String, interfaces: Array[String]) { - isPublic = ResourceOrProviderClassVisitor.isPublic(access) - className = name - isAbstract = ResourceOrProviderClassVisitor.isAbstract(access) - } - - override def visitInnerClass(name: String, outerName: String, innerName: String, access: Int) { - assert (className != null) - - if (name == className) { - isInnerClass = true - isStatic = ResourceOrProviderClassVisitor.isStatic(access) - } - } - - override def visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor = { - isAnnotated |= ResourceOrProviderClassVisitor.annotationClassDescriptors(desc) - null - } -} - - -object ResourceOrProviderClassVisitor { - val annotationClassDescriptors = Set(classOf[Path], classOf[Provider]) map Type.getDescriptor - - def isPublic = isSet(Opcodes.ACC_PUBLIC) _ - def isStatic = isSet(Opcodes.ACC_STATIC) _ - def isAbstract = isSet(Opcodes.ACC_ABSTRACT) _ - - private def isSet(bits: Int)(access: Int): Boolean = (access & bits) == bits - - def visit(classReader: ClassReader): ResourceOrProviderClassVisitor = { - val visitor = new ResourceOrProviderClassVisitor - classReader.accept(visitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES) - visitor - } -} diff --git a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java index 80c1cb8b458..77411fc080e 100644 --- a/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java +++ b/jdisc_http_service/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerConformanceTest.java @@ -323,7 +323,7 @@ public class HttpServerConformanceTest extends ServerProviderConformanceTest { @Override @Test public void testRequestContentWriteExceptionAfterResponseWriteWithSyncCompletion() throws Throwable { - new TestRunner().expect(success()) + new TestRunner().expect(anyOf(success(), successNoContent())) .execute(); } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java index bd75368a0dc..ff85c49bb13 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/identity/AthenzCredentialsMaintainer.java @@ -1,6 +1,8 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.maintenance.identity; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.yahoo.vespa.athenz.api.AthenzService; import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient; import com.yahoo.vespa.athenz.client.zts.InstanceIdentity; @@ -9,7 +11,7 @@ import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper; import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocumentClient; import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; +import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocumentEntity; import com.yahoo.vespa.athenz.identityprovider.client.DefaultIdentityDocumentClient; import com.yahoo.vespa.athenz.identityprovider.client.InstanceCsrGenerator; import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; @@ -19,9 +21,9 @@ import com.yahoo.vespa.athenz.tls.KeyUtils; import com.yahoo.vespa.athenz.tls.Pkcs10Csr; import com.yahoo.vespa.athenz.tls.SslContextBuilder; import com.yahoo.vespa.athenz.tls.X509CertificateUtils; +import com.yahoo.vespa.athenz.utils.SiaUtils; import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.node.admin.component.Environment; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeSpec; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import javax.net.ssl.SSLContext; @@ -38,7 +40,6 @@ import java.security.cert.X509Certificate; import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Set; import static java.util.Collections.singleton; @@ -53,12 +54,15 @@ public class AthenzCredentialsMaintainer { private static final Duration REFRESH_PERIOD = Duration.ofDays(1); private static final Path CONTAINER_SIA_DIRECTORY = Paths.get("/var/lib/sia"); + private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule()); + private final boolean enabled; private final PrefixLogger log; private final String hostname; private final Path trustStorePath; private final Path privateKeyFile; private final Path certificateFile; + private final Path identityDocumentFile; private final AthenzService containerIdentity; private final URI ztsEndpoint; private final Clock clock; @@ -66,8 +70,6 @@ public class AthenzCredentialsMaintainer { private final IdentityDocumentClient identityDocumentClient; private final InstanceCsrGenerator csrGenerator; private final AthenzService configserverIdentity; - private final String zoneRegion; - private final String zoneEnvironment; public AthenzCredentialsMaintainer(String hostname, Environment environment, @@ -82,8 +84,9 @@ public class AthenzCredentialsMaintainer { this.configserverIdentity = environment.getConfigserverAthenzIdentity(); this.csrGenerator = new InstanceCsrGenerator(environment.getCertificateDnsSuffix()); this.trustStorePath = environment.getTrustStorePath(); - this.privateKeyFile = getPrivateKeyFile(containerSiaDirectory, containerIdentity); - this.certificateFile = getCertificateFile(containerSiaDirectory, containerIdentity); + this.privateKeyFile = SiaUtils.getPrivateKeyFile(containerSiaDirectory, containerIdentity); + this.certificateFile = SiaUtils.getCertificateFile(containerSiaDirectory, containerIdentity); + this.identityDocumentFile = containerSiaDirectory.resolve("vespa-node-identity-document.json"); this.hostIdentityProvider = hostIdentityProvider; this.identityDocumentClient = new DefaultIdentityDocumentClient( @@ -91,15 +94,12 @@ public class AthenzCredentialsMaintainer { hostIdentityProvider, new AthenzIdentityVerifier(singleton(configserverIdentity))); this.clock = Clock.systemUTC(); - this.zoneRegion = environment.getRegion(); - this.zoneEnvironment = environment.getEnvironment(); } /** - * @param nodeSpec Node specification * @return Returns true if credentials were updated */ - public boolean converge(NodeSpec nodeSpec) { + public boolean converge() { try { if (!enabled) { log.debug("Feature disabled on this host - not fetching certificate"); @@ -107,26 +107,25 @@ public class AthenzCredentialsMaintainer { } log.debug("Checking certificate"); Instant now = clock.instant(); - VespaUniqueInstanceId instanceId = getVespaUniqueInstanceId(nodeSpec); - Set<String> ipAddresses = nodeSpec.getIpAddresses(); - if (!Files.exists(privateKeyFile) || !Files.exists(certificateFile)) { - log.info("Certificate and/or private key file does not exist"); + if (!Files.exists(privateKeyFile) || !Files.exists(certificateFile) || !Files.exists(identityDocumentFile)) { + log.info("Certificate/private key/identity document file does not exist"); Files.createDirectories(privateKeyFile.getParent()); Files.createDirectories(certificateFile.getParent()); - registerIdentity(instanceId, ipAddresses); + Files.createDirectories(identityDocumentFile.getParent()); + registerIdentity(); return true; } X509Certificate certificate = readCertificateFromFile(); Instant expiry = certificate.getNotAfter().toInstant(); if (isCertificateExpired(expiry, now)) { log.info(String.format("Certificate has expired (expiry=%s)", expiry.toString())); - registerIdentity(instanceId, ipAddresses); + registerIdentity(); return true; } Duration age = Duration.between(certificate.getNotBefore().toInstant(), now); if (shouldRefreshCredentials(age)) { log.info(String.format("Certificate is ready to be refreshed (age=%s)", age.toString())); - refreshIdentity(instanceId, ipAddresses); + refreshIdentity(); return true; } log.debug("Certificate is still valid"); @@ -148,20 +147,6 @@ public class AthenzCredentialsMaintainer { } } - @SuppressWarnings("deprecation") - private VespaUniqueInstanceId getVespaUniqueInstanceId(NodeSpec nodeSpec) { - NodeSpec.Membership membership = nodeSpec.getMembership().get(); - NodeSpec.Owner owner = nodeSpec.getOwner().get(); - return new VespaUniqueInstanceId( - membership.getIndex(), - membership.getClusterId(), - owner.getInstance(), - owner.getApplication(), - owner.getTenant(), - zoneRegion, - zoneEnvironment); - } - private boolean shouldRefreshCredentials(Duration age) { return age.compareTo(REFRESH_PERIOD) >= 0; } @@ -175,32 +160,32 @@ public class AthenzCredentialsMaintainer { return now.isAfter(expiry.minus(EXPIRY_MARGIN)); } - private void registerIdentity(VespaUniqueInstanceId instanceId, Set<String> ipAddresses) { + private void registerIdentity() { KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, instanceId, ipAddresses, keyPair); SignedIdentityDocument signedIdentityDocument = identityDocumentClient.getNodeIdentityDocument(hostname); + Pkcs10Csr csr = csrGenerator.generateCsr( + containerIdentity, signedIdentityDocument.providerUniqueId(), signedIdentityDocument.ipAddresses(), keyPair); try (ZtsClient ztsClient = new DefaultZtsClient(ztsEndpoint, hostIdentityProvider)) { InstanceIdentity instanceIdentity = ztsClient.registerInstance( configserverIdentity, containerIdentity, - instanceId.asDottedString(), + signedIdentityDocument.providerUniqueId().asDottedString(), EntityBindingsMapper.toAttestationData(signedIdentityDocument), false, csr); + writeIdentityDocument(signedIdentityDocument); writePrivateKeyAndCertificate(keyPair.getPrivate(), instanceIdentity.certificate()); log.info("Instance successfully registered and credentials written to file"); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (Exception e) { - // TODO Change close() in ZtsClient to not throw checked exception - throw new RuntimeException(e); } } - private void refreshIdentity(VespaUniqueInstanceId instanceId, Set<String> ipAddresses) { + private void refreshIdentity() { + SignedIdentityDocument identityDocument = readIdentityDocument(); KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA); - Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, instanceId, ipAddresses, keyPair); + Pkcs10Csr csr = csrGenerator.generateCsr(containerIdentity, identityDocument.providerUniqueId(), identityDocument.ipAddresses(), keyPair); SSLContext containerIdentitySslContext = new SslContextBuilder() .withKeyStore(privateKeyFile.toFile(), certificateFile.toFile()) @@ -211,16 +196,34 @@ public class AthenzCredentialsMaintainer { ztsClient.refreshInstance( configserverIdentity, containerIdentity, - instanceId.asDottedString(), + identityDocument.providerUniqueId().asDottedString(), false, csr); writePrivateKeyAndCertificate(keyPair.getPrivate(), instanceIdentity.certificate()); log.info("Instance successfully refreshed and credentials written to file"); } catch (IOException e) { throw new UncheckedIOException(e); - } catch (Exception e) { - // TODO Change close() in ZtsClient to not throw checked exception - throw new RuntimeException(e); + } + } + + private SignedIdentityDocument readIdentityDocument() { + try { + SignedIdentityDocumentEntity entity = mapper.readValue(identityDocumentFile.toFile(), SignedIdentityDocumentEntity.class); + return EntityBindingsMapper.toSignedIdentityDocument(entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private void writeIdentityDocument(SignedIdentityDocument signedIdentityDocument) { + try { + SignedIdentityDocumentEntity entity = + EntityBindingsMapper.toSignedIdentityDocumentEntity(signedIdentityDocument); + Path tempIdentityDocumentFile = toTempPath(identityDocumentFile); + mapper.writeValue(tempIdentityDocumentFile.toFile(), entity); + Files.move(tempIdentityDocumentFile, identityDocumentFile, StandardCopyOption.ATOMIC_MOVE); + } catch (IOException e) { + throw new UncheckedIOException(e); } } @@ -238,18 +241,4 @@ public class AthenzCredentialsMaintainer { return Paths.get(file.toAbsolutePath().toString() + ".tmp"); } - // TODO Move to vespa-athenz - private static Path getPrivateKeyFile(Path root, AthenzService service) { - return root - .resolve("keys") - .resolve(String.format("%s.%s.key.pem", service.getDomain().getName(), service.getName())); - } - - // TODO Move to vespa-athenz - private static Path getCertificateFile(Path root, AthenzService service) { - return root - .resolve("certs") - .resolve(String.format("%s.%s.cert.pem", service.getDomain().getName(), service.getName())); - } - } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java index 7fa9a90b744..5f1b7aefcfe 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/nodeagent/NodeAgentImpl.java @@ -498,7 +498,7 @@ public class NodeAgentImpl implements NodeAgent { runLocalResumeScriptIfNeeded(node); - athenzCredentialsMaintainer.converge(node); + athenzCredentialsMaintainer.converge(); doBeforeConverge(node); diff --git a/searchsummary/CMakeLists.txt b/searchsummary/CMakeLists.txt index 5f6e8881f13..4df636e0219 100644 --- a/searchsummary/CMakeLists.txt +++ b/searchsummary/CMakeLists.txt @@ -24,6 +24,7 @@ vespa_define_module( TESTS src/tests/docsumformat src/tests/docsummary + src/tests/docsummary/attribute_combiner src/tests/docsummary/slime_summary src/tests/extractkeywords ) diff --git a/searchsummary/src/tests/docsummary/attribute_combiner/CMakeLists.txt b/searchsummary/src/tests/docsummary/attribute_combiner/CMakeLists.txt new file mode 100644 index 00000000000..df323b9c982 --- /dev/null +++ b/searchsummary/src/tests/docsummary/attribute_combiner/CMakeLists.txt @@ -0,0 +1,8 @@ +# Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +vespa_add_executable(searchsummary_attribute_combiner_test_app TEST + SOURCES + attribute_combiner_test.cpp + DEPENDS + searchsummary +) +vespa_add_test(NAME searchsummary_attribute_combiner_test_app COMMAND searchsummary_attribute_combiner_test_app) diff --git a/searchsummary/src/tests/docsummary/attribute_combiner/attribute_combiner_test.cpp b/searchsummary/src/tests/docsummary/attribute_combiner/attribute_combiner_test.cpp new file mode 100644 index 00000000000..97fafd0a446 --- /dev/null +++ b/searchsummary/src/tests/docsummary/attribute_combiner/attribute_combiner_test.cpp @@ -0,0 +1,217 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include <vespa/searchcommon/common/undefinedvalues.h> +#include <vespa/searchlib/attribute/attributefactory.h> +#include <vespa/searchlib/attribute/attributemanager.h> +#include <vespa/searchlib/attribute/attributevector.h> +#include <vespa/searchlib/attribute/attributevector.hpp> +#include <vespa/searchlib/attribute/floatbase.h> +#include <vespa/searchlib/attribute/integerbase.h> +#include <vespa/searchlib/attribute/stringbase.h> +#include <vespa/searchlib/util/slime_output_raw_buf_adapter.h> +#include <vespa/searchsummary/docsummary/docsumstate.h> +#include <vespa/searchsummary/docsummary/docsum_field_writer_state.h> +#include <vespa/searchsummary/docsummary/attribute_combiner_dfw.h> +#include <vespa/vespalib/data/slime/slime.h> +#include <vespa/vespalib/testkit/testapp.h> + +#include <vespa/log/log.h> +LOG_SETUP("attribute_combiner_test"); + +using search::AttributeFactory; +using search::AttributeManager; +using search::AttributeVector; +using search::IntegerAttribute; +using search::FloatingPointAttribute; +using search::StringAttribute; +using search::attribute::BasicType; +using search::attribute::CollectionType; +using search::attribute::Config; +using search::attribute::IAttributeVector; +using search::attribute::getUndefined; +using search::docsummary::AttributeCombinerDFW; +using search::docsummary::GetDocsumsState; +using search::docsummary::GetDocsumsStateCallback; +using search::docsummary::IDocsumEnvironment; +using search::docsummary::IDocsumFieldWriter; + +namespace { + +vespalib::string +toCompactJsonString(const vespalib::Slime &slime) +{ + vespalib::SimpleBuffer buf; + vespalib::slime::JsonFormat::encode(slime, buf, true); + return buf.get().make_string(); +} + +struct FieldBlock { + vespalib::string input; + vespalib::Slime slime; + search::RawBuf binary; + vespalib::string json; + + explicit FieldBlock(const vespalib::string &jsonInput) + : input(jsonInput), slime(), binary(1024), json() + { + size_t used = vespalib::slime::JsonFormat::decode(jsonInput, slime); + EXPECT_TRUE(used > 0); + json = toCompactJsonString(slime); + search::SlimeOutputRawBufAdapter adapter(binary); + vespalib::slime::BinaryFormat::encode(slime, adapter); + } + const char *data() const { return binary.GetDrainPos(); } + size_t dataLen() const { return binary.GetUsedLen(); } +}; + +struct AttributeManagerFixture +{ + AttributeManager mgr; + + AttributeManagerFixture(); + + ~AttributeManagerFixture(); + + template <typename AttributeType, typename ValueType> + void + buildAttribute(const vespalib::string &name, + BasicType type, + std::vector<std::vector<ValueType>> values); + + void + buildStringAttribute(const vespalib::string &name, + std::vector<std::vector<vespalib::string>> values); + void + buildFloatAttribute(const vespalib::string &name, + std::vector<std::vector<double>> values); + + void + buildIntegerAttribute(const vespalib::string &name, + BasicType type, + std::vector<std::vector<IAttributeVector::largeint_t>> values); +}; + +AttributeManagerFixture::AttributeManagerFixture() + : mgr() +{ + buildStringAttribute("array.name", {{"n1.1", "n1.2"}, {"n2"}, {"n3.1", "n3.2"}, {"", "n4.2"}}); + buildIntegerAttribute("array.val", BasicType::Type::INT8, {{ 10, 11}, {20, 21 }, {30}, { getUndefined<int8_t>(), 41}}); + buildFloatAttribute("array.fval", {{ 110.0}, { 120.0, 121.0 }, { 130.0, 131.0}, { getUndefined<double>(), 141.0 }}); +} + +AttributeManagerFixture::~AttributeManagerFixture() = default; + +template <typename AttributeType, typename ValueType> +void +AttributeManagerFixture::buildAttribute(const vespalib::string &name, + BasicType type, + std::vector<std::vector<ValueType>> values) +{ + Config cfg(type, CollectionType::Type::ARRAY); + auto attrBase = AttributeFactory::createAttribute(name, cfg); + EXPECT_TRUE(attrBase); + auto attr = std::dynamic_pointer_cast<AttributeType>(attrBase); + EXPECT_TRUE(attr); + attr->addReservedDoc(); + for (const auto &docValues : values) { + uint32_t docId = 0; + EXPECT_TRUE(attr->addDoc(docId)); + EXPECT_NOT_EQUAL(0u, docId); + for (const auto &value : docValues) { + attr->append(docId, value, 1); + } + attr->commit(); + } + EXPECT_TRUE(mgr.add(attr)); +} + +void +AttributeManagerFixture::buildStringAttribute(const vespalib::string &name, + std::vector<std::vector<vespalib::string>> values) +{ + buildAttribute<StringAttribute, vespalib::string>(name, BasicType::Type::STRING, std::move(values)); +} + +void +AttributeManagerFixture::buildFloatAttribute(const vespalib::string &name, + std::vector<std::vector<double>> values) +{ + buildAttribute<FloatingPointAttribute, double>(name, BasicType::Type::DOUBLE, std::move(values)); +} + +void +AttributeManagerFixture::buildIntegerAttribute(const vespalib::string &name, + BasicType type, + std::vector<std::vector<IAttributeVector::largeint_t>> values) +{ + buildAttribute<IntegerAttribute, IAttributeVector::largeint_t>(name, type, std::move(values)); +} + + +class DummyStateCallback : public GetDocsumsStateCallback +{ +public: + void FillSummaryFeatures(GetDocsumsState *, IDocsumEnvironment *) override { } + void FillRankFeatures(GetDocsumsState *, IDocsumEnvironment *) override { } + void ParseLocation(GetDocsumsState *) override { } + ~DummyStateCallback() override { } +}; + + +struct Fixture +{ + AttributeManagerFixture attrs; + std::unique_ptr<IDocsumFieldWriter> writer; + DummyStateCallback stateCallback; + GetDocsumsState state; + + Fixture(); + ~Fixture(); + void assertWritten(const vespalib::string &exp, uint32_t docId); +}; + +Fixture::Fixture() + : attrs(), + writer(AttributeCombinerDFW::create("array", attrs.mgr)), + stateCallback(), + state(stateCallback) +{ + EXPECT_TRUE(writer->setFieldWriterStateIndex(0)); + state._attrCtx = attrs.mgr.createContext(); + state._fieldWriterStates.resize(1); +} + +Fixture::~Fixture() +{ +} + +void +Fixture::assertWritten(const vespalib::string &expectedJson, uint32_t docId) +{ + vespalib::Slime target; + vespalib::slime::SlimeInserter inserter(target); + writer->insertField(docId, nullptr, &state, search::docsummary::RES_JSONSTRING, inserter); + search::RawBuf binary(1024); + vespalib::string json = toCompactJsonString(target); + search::SlimeOutputRawBufAdapter adapter(binary); + vespalib::slime::BinaryFormat::encode(target, adapter); + FieldBlock block(expectedJson); + if (!EXPECT_EQUAL(block.dataLen(), binary.GetUsedLen()) || + !EXPECT_EQUAL(0, memcmp(block.data(), binary.GetDrainPos(), block.dataLen()))) { + LOG(error, "Expected '%s'", expectedJson.c_str()); + LOG(error, "Expected normalized '%s'", block.json.c_str()); + LOG(error, "Got '%s'", json.c_str()); + } +} + +TEST_F("require that attributes combiner dfw generates correct slime output for array of struct", Fixture()) +{ + f.assertWritten("[ { fval: 110.0, name: \"n1.1\", val: 10}, { name: \"n1.2\", val: 11}]", 1); + f.assertWritten("[ { fval: 120.0, name: \"n2\", val: 20}, { fval: 121.0, val: 21 }]", 2); + f.assertWritten("[ { fval: 130.0, name: \"n3.1\", val: 30}, { fval: 131.0, name: \"n3.2\"} ]", 3); + f.assertWritten("[ { }, { fval: 141.0, name: \"n4.2\", val: 41} ]", 4); +} + +} + +TEST_MAIN() { TEST_RUN_ALL(); } diff --git a/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt b/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt index 9009f0bcbc7..ce54e7b0ea7 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt +++ b/searchsummary/src/vespa/searchsummary/docsummary/CMakeLists.txt @@ -1,6 +1,9 @@ # Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. vespa_add_library(searchsummary_docsummary OBJECT SOURCES + array_attribute_combiner_dfw.cpp + attribute_combiner_dfw.cpp + attribute_field_writer.cpp resultclass.cpp resultconfig.cpp resultpacker.cpp diff --git a/searchsummary/src/vespa/searchsummary/docsummary/array_attribute_combiner_dfw.cpp b/searchsummary/src/vespa/searchsummary/docsummary/array_attribute_combiner_dfw.cpp new file mode 100644 index 00000000000..84e329f159d --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/array_attribute_combiner_dfw.cpp @@ -0,0 +1,89 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "array_attribute_combiner_dfw.h" +#include "docsum_field_writer_state.h" +#include "attribute_field_writer.h" +#include <vespa/searchcommon/attribute/iattributecontext.h> +#include <vespa/searchcommon/attribute/iattributevector.h> +#include <vespa/vespalib/data/slime/cursor.h> + +using search::attribute::IAttributeContext; +using search::attribute::IAttributeVector; +using vespalib::slime::Cursor; + +namespace search::docsummary { + +namespace { + +class ArrayAttributeFieldWriterState : public DocsumFieldWriterState +{ + std::vector<std::unique_ptr<AttributeFieldWriter>> _writers; + +public: + ArrayAttributeFieldWriterState(const std::vector<vespalib::string> &fieldNames, + const std::vector<vespalib::string> &attributeNames, + IAttributeContext &context); + ~ArrayAttributeFieldWriterState() override; + void insertField(uint32_t docId, vespalib::slime::Inserter &target) override; +}; + +ArrayAttributeFieldWriterState::ArrayAttributeFieldWriterState(const std::vector<vespalib::string> &fieldNames, + const std::vector<vespalib::string> &attributeNames, + IAttributeContext &context) + : DocsumFieldWriterState() +{ + size_t fields = fieldNames.size(); + _writers.reserve(fields); + for (uint32_t field = 0; field < fields; ++field) { + const IAttributeVector *attr = context.getAttribute(attributeNames[field]); + if (attr != nullptr) { + _writers.emplace_back(AttributeFieldWriter::create(fieldNames[field], *attr)); + } + } +} + +ArrayAttributeFieldWriterState::~ArrayAttributeFieldWriterState() = default; + +void +ArrayAttributeFieldWriterState::insertField(uint32_t docId, vespalib::slime::Inserter &target) +{ + uint32_t elems = 0; + for (auto &writer : _writers) { + writer->fetch(docId); + if (elems < writer->size()) { + elems = writer->size(); + } + } + Cursor &arr = target.insertArray(); + for (uint32_t idx = 0; idx < elems; ++idx) { + Cursor &obj = arr.addObject(); + for (auto &writer : _writers) { + writer->print(idx, obj); + } + } +} + +} + +ArrayAttributeCombinerDFW::ArrayAttributeCombinerDFW(const vespalib::string &fieldName, + const std::vector<vespalib::string> &fields) + : AttributeCombinerDFW(fieldName), + _fields(fields), + _attributeNames() +{ + _attributeNames.reserve(_fields.size()); + vespalib::string prefix = fieldName + "."; + for (const auto &field : _fields) { + _attributeNames.emplace_back(prefix + field); + } +} + +ArrayAttributeCombinerDFW::~ArrayAttributeCombinerDFW() = default; + +std::unique_ptr<DocsumFieldWriterState> +ArrayAttributeCombinerDFW::allocFieldWriterState(IAttributeContext &context) +{ + return std::make_unique<ArrayAttributeFieldWriterState>(_fields, _attributeNames, context); +} + +} diff --git a/searchsummary/src/vespa/searchsummary/docsummary/array_attribute_combiner_dfw.h b/searchsummary/src/vespa/searchsummary/docsummary/array_attribute_combiner_dfw.h new file mode 100644 index 00000000000..c02d2bd5da6 --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/array_attribute_combiner_dfw.h @@ -0,0 +1,29 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "attribute_combiner_dfw.h" + +namespace search::attribute { class IAttributeContext; } + +namespace search::docsummary { + +class DocsumFieldWriterState; + +/* + * This class reads values from multiple struct field attributes and + * inserts them as an array of struct. + */ +class ArrayAttributeCombinerDFW : public AttributeCombinerDFW +{ + std::vector<vespalib::string> _fields; + std::vector<vespalib::string> _attributeNames; + + std::unique_ptr<DocsumFieldWriterState> allocFieldWriterState(search::attribute::IAttributeContext &context) override; +public: + ArrayAttributeCombinerDFW(const vespalib::string &fieldName, + const std::vector<vespalib::string> &fields); + ~ArrayAttributeCombinerDFW() override; +}; + +} diff --git a/searchsummary/src/vespa/searchsummary/docsummary/attribute_combiner_dfw.cpp b/searchsummary/src/vespa/searchsummary/docsummary/attribute_combiner_dfw.cpp new file mode 100644 index 00000000000..b532cfb273a --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/attribute_combiner_dfw.cpp @@ -0,0 +1,141 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "attribute_combiner_dfw.h" +#include "array_attribute_combiner_dfw.h" +#include "docsum_field_writer_state.h" +#include "docsumstate.h" +#include <vespa/searchlib/attribute/attributeguard.h> +#include <vespa/searchlib/attribute/attributevector.h> +#include <vespa/searchlib/attribute/iattributemanager.h> +#include <algorithm> + +#include <vespa/log/log.h> +LOG_SETUP(".searchsummary.docsummary.attribute_combiner_dfw"); + +using search::AttributeGuard; +using search::AttributeVector; +using search::attribute::CollectionType; + +namespace search::docsummary { + +namespace { + +class StructFields +{ + std::vector<vespalib::string> _mapFields; + std::vector<vespalib::string> _arrayFields; + bool _hasMapKey; + bool _error; + +public: + StructFields(const vespalib::string &fieldName, const IAttributeManager &attrMgr); + ~StructFields(); + const std::vector<vespalib::string> &getMapFields() const { return _mapFields; } + const std::vector<vespalib::string> &getArrayFields() const { return _arrayFields; } + bool hasMapKey() const { return _hasMapKey; } + bool getError() const { return _error; } +}; + + +StructFields::StructFields(const vespalib::string &fieldName, const IAttributeManager &attrMgr) + : _mapFields(), + _arrayFields(), + _hasMapKey(false), + _error(false) +{ + // Note: Doesn't handle imported attributes + std::vector<AttributeGuard> attrs; + attrMgr.getAttributeList(attrs); + vespalib::string prefix = fieldName + "."; + vespalib::string keyName = prefix + "key"; + vespalib::string valuePrefix = prefix + "value."; + for (const auto &guard : attrs) { + vespalib::string name = guard->getName(); + if (name.substr(0, prefix.size()) != prefix) { + continue; + } + auto collType = guard->getCollectionType(); + if (collType != CollectionType::Type::ARRAY) { + LOG(warning, "Attribute %s is not an array attribute", name.c_str()); + _error = true; + break; + } + if (name.substr(0, valuePrefix.size()) == valuePrefix) { + _mapFields.emplace_back(name.substr(valuePrefix.size())); + } else { + _arrayFields.emplace_back(name.substr(prefix.size())); + if (name == keyName) { + _hasMapKey = true; + } + } + } + if (!_error) { + std::sort(_arrayFields.begin(), _arrayFields.end()); + std::sort(_mapFields.begin(), _mapFields.end()); + if (!_mapFields.empty()) { + if (!_hasMapKey) { + LOG(warning, "Missing key attribute '%s', have value attributes for map", keyName.c_str()); + _error = true; + } else if (_arrayFields.size() != 1u) { + LOG(warning, "Could not determine if field '%s' is array or map of struct", fieldName.c_str()); + _error = true; + } + } + } +} + +StructFields::~StructFields() = default; + +} + +AttributeCombinerDFW::AttributeCombinerDFW(const vespalib::string &fieldName) + : IDocsumFieldWriter(), + _stateIndex(0), + _fieldName(fieldName) +{ +} + +AttributeCombinerDFW::~AttributeCombinerDFW() = default; + +bool +AttributeCombinerDFW::IsGenerated() const +{ + return true; +} + +bool +AttributeCombinerDFW::setFieldWriterStateIndex(uint32_t fieldWriterStateIndex) +{ + _stateIndex = fieldWriterStateIndex; + return true; +} + +std::unique_ptr<IDocsumFieldWriter> +AttributeCombinerDFW::create(const vespalib::string &fieldName, IAttributeManager &attrMgr) +{ + StructFields structFields(fieldName, attrMgr); + if (structFields.getError()) { + return std::unique_ptr<IDocsumFieldWriter>(); + } else if (!structFields.getMapFields().empty()) { + LOG(warning, "map of struct is not yet supported for field '%s'", fieldName.c_str()); + return std::unique_ptr<IDocsumFieldWriter>(); + } + return std::make_unique<ArrayAttributeCombinerDFW>(fieldName, structFields.getArrayFields()); +} + +void +AttributeCombinerDFW::insertField(uint32_t docid, + GeneralResult *, + GetDocsumsState *state, + ResType, + vespalib::slime::Inserter &target) +{ + auto &fieldWriterState = state->_fieldWriterStates[_stateIndex]; + if (!fieldWriterState) { + fieldWriterState = allocFieldWriterState(*state->_attrCtx); + } + fieldWriterState->insertField(docid, target); +} + +} + diff --git a/searchsummary/src/vespa/searchsummary/docsummary/attribute_combiner_dfw.h b/searchsummary/src/vespa/searchsummary/docsummary/attribute_combiner_dfw.h new file mode 100644 index 00000000000..ef54522a923 --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/attribute_combiner_dfw.h @@ -0,0 +1,36 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include "docsumfieldwriter.h" + +namespace search::attribute { class IAttributeContext; } + +namespace search::docsummary { + +class DocsumFieldWriterState; +class DynamicDocsumWriter; + +/* + * This class reads values from multiple struct field attributes and + * inserts them as an array of struct or a map of struct. + */ +class AttributeCombinerDFW : public IDocsumFieldWriter +{ +protected: + uint32_t _stateIndex; + vespalib::string _fieldName; + AttributeCombinerDFW(const vespalib::string &fieldName); +protected: + virtual std::unique_ptr<DocsumFieldWriterState> allocFieldWriterState(search::attribute::IAttributeContext &context) = 0; +public: + ~AttributeCombinerDFW() override; + bool IsGenerated() const override; + bool setFieldWriterStateIndex(uint32_t fieldWriterStateIndex) override; + static std::unique_ptr<IDocsumFieldWriter> create(const vespalib::string &fieldName, IAttributeManager &attrMgr); + void insertField(uint32_t docid, GeneralResult *gres, GetDocsumsState *state, + ResType type, vespalib::slime::Inserter &target) override; +}; + +} + diff --git a/searchsummary/src/vespa/searchsummary/docsummary/attribute_field_writer.cpp b/searchsummary/src/vespa/searchsummary/docsummary/attribute_field_writer.cpp new file mode 100644 index 00000000000..2eebe7137dc --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/attribute_field_writer.cpp @@ -0,0 +1,172 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#include "attribute_field_writer.h" +#include <vespa/searchcommon/attribute/attributecontent.h> +#include <vespa/searchcommon/common/undefinedvalues.h> +#include <vespa/vespalib/data/slime/cursor.h> +#include <cassert> + +using search::attribute::BasicType; +using search::attribute::IAttributeVector; +using search::attribute::getUndefined; +using vespalib::slime::Cursor; + +namespace search::docsummary { + +AttributeFieldWriter::AttributeFieldWriter(const vespalib::string &fieldName, + const IAttributeVector &attr) + : _fieldName(fieldName), + _attr(attr), + _size(0) +{ +} + +AttributeFieldWriter::~AttributeFieldWriter() = default; + +namespace { + +template <class Content> +class WriteField : public AttributeFieldWriter +{ +protected: + Content _content; + + WriteField(const vespalib::string &fieldName, const IAttributeVector &attr); + ~WriteField() override; +private: + void fetch(uint32_t docId) override; +}; + +class WriteStringField : public WriteField<search::attribute::ConstCharContent> +{ +public: + WriteStringField(const vespalib::string &fieldName, + const IAttributeVector &attr); + ~WriteStringField() override; + void print(uint32_t idx, Cursor &cursor) override; +}; + + +class WriteFloatField : public WriteField<search::attribute::FloatContent> +{ +public: + WriteFloatField(const vespalib::string &fieldName, + const IAttributeVector &attr); + ~WriteFloatField() override; + void print(uint32_t idx, Cursor &cursor) override; +}; + +class WriteIntField : public WriteField<search::attribute::IntegerContent> +{ + IAttributeVector::largeint_t _undefined; +public: + WriteIntField(const vespalib::string &fieldName, + const IAttributeVector &attr, + IAttributeVector::largeint_t undefined); + ~WriteIntField() override; + void print(uint32_t idx, Cursor &cursor) override; +}; + +template <class Content> +WriteField<Content>::WriteField(const vespalib::string &fieldName, const IAttributeVector &attr) + : AttributeFieldWriter(fieldName, attr), + _content() +{ +} + +template <class Content> +WriteField<Content>::~WriteField() = default; + +template <class Content> +void +WriteField<Content>::fetch(uint32_t docId) +{ + _content.fill(_attr, docId); + _size = _content.size(); +} + +WriteStringField::WriteStringField(const vespalib::string &fieldName, + const IAttributeVector &attr) + : WriteField(fieldName, attr) +{ +} + +WriteStringField::~WriteStringField() = default; + +void +WriteStringField::print(uint32_t idx, Cursor &cursor) +{ + if (idx < _size) { + const char *s = _content[idx]; + if (s[0] != '\0') { + cursor.setString(_fieldName, vespalib::Memory(s)); + } + } +} + +WriteFloatField::WriteFloatField(const vespalib::string &fieldName, + const IAttributeVector &attr) + : WriteField(fieldName, attr) +{ +} + +WriteFloatField::~WriteFloatField() = default; + +void +WriteFloatField::print(uint32_t idx, Cursor &cursor) +{ + if (idx < _size) { + double val = _content[idx]; + if (!search::attribute::isUndefined(val)) { + cursor.setDouble(_fieldName, val); + } + } +} + +WriteIntField::WriteIntField(const vespalib::string &fieldName, + const IAttributeVector &attr, + IAttributeVector::largeint_t undefined) + : WriteField(fieldName, attr), + _undefined(undefined) +{ +} + +WriteIntField::~WriteIntField() = default; + +void +WriteIntField::print(uint32_t idx, Cursor &cursor) +{ + if (idx < _size) { + auto val = _content[idx]; + if (val != _undefined) { + cursor.setLong(_fieldName, _content[idx]); + } + } +} + +} + +std::unique_ptr<AttributeFieldWriter> +AttributeFieldWriter::create(const vespalib::string &fieldName, const IAttributeVector &attr) +{ + switch (attr.getBasicType()) { + case BasicType::INT8: + return std::make_unique<WriteIntField>(fieldName, attr, getUndefined<int8_t>()); + case BasicType::INT16: + return std::make_unique<WriteIntField>(fieldName, attr, getUndefined<int16_t>()); + case BasicType::INT32: + return std::make_unique<WriteIntField>(fieldName, attr, getUndefined<int32_t>()); + case BasicType::INT64: + return std::make_unique<WriteIntField>(fieldName, attr, getUndefined<int64_t>()); + case BasicType::FLOAT: + case BasicType::DOUBLE: + return std::make_unique<WriteFloatField>(fieldName, attr); + case BasicType::STRING: + return std::make_unique<WriteStringField>(fieldName, attr); + default: + assert(false); + abort(); + } +} + +} diff --git a/searchsummary/src/vespa/searchsummary/docsummary/attribute_field_writer.h b/searchsummary/src/vespa/searchsummary/docsummary/attribute_field_writer.h new file mode 100644 index 00000000000..104455a0e79 --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/attribute_field_writer.h @@ -0,0 +1,34 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +#include <vespa/vespalib/data/memory.h> + +namespace search::attribute { class IAttributeVector; } +namespace vespalib::slime { class Cursor; } + +namespace search::docsummary { + +/* + * This class reads values from a struct field attribute and inserts + * them into proper position in an array of struct or map of struct. + * If the value to be inserted is considered to be undefined then + * the value is not inserted. + */ +class AttributeFieldWriter +{ +protected: + const vespalib::Memory _fieldName; + const search::attribute::IAttributeVector &_attr; + size_t _size; +public: + AttributeFieldWriter(const vespalib::string &fieldName, + const search::attribute::IAttributeVector &attr); + virtual ~AttributeFieldWriter(); + virtual void fetch(uint32_t docId) = 0; + virtual void print(uint32_t idx, vespalib::slime::Cursor &cursor) = 0; + static std::unique_ptr<AttributeFieldWriter> create(const vespalib::string &fieldName, const search::attribute::IAttributeVector &attr); + uint32_t size() const { return _size; } +}; + +} diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsum_field_writer_state.h b/searchsummary/src/vespa/searchsummary/docsummary/docsum_field_writer_state.h new file mode 100644 index 00000000000..940cfd6ce06 --- /dev/null +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsum_field_writer_state.h @@ -0,0 +1,21 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +#pragma once + +namespace vespalib::slime { class Inserter; } + +namespace search::docsummary { + +/* + * A subclass of this class can be instantiated by a document field writer to + * track extra state during handling of a document summary request and + * insert the field value using that state. + */ +class DocsumFieldWriterState +{ +public: + virtual void insertField(uint32_t docId, vespalib::slime::Inserter &target) = 0; + virtual ~DocsumFieldWriterState() = default; +}; + +} diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.cpp b/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.cpp index 7b463352155..18e7e471663 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.cpp +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.cpp @@ -21,6 +21,12 @@ using search::common::Location; const vespalib::string IDocsumFieldWriter::_empty(""); +bool +IDocsumFieldWriter::setFieldWriterStateIndex(uint32_t) +{ + return false; // Don't need any field writer state by default +} + //-------------------------------------------------------------------------- EmptyDFW::EmptyDFW() { } diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.h b/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.h index abce5c12227..51079f7736e 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.h +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsumfieldwriter.h @@ -40,6 +40,7 @@ public: } void setIndex(size_t v) { _index = v; } size_t getIndex() const { return _index; } + virtual bool setFieldWriterStateIndex(uint32_t fieldWriterStateIndex); private: size_t _index; static const vespalib::string _empty; diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.cpp b/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.cpp index 91953612f6a..b0431b6e6ac 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.cpp +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.cpp @@ -4,6 +4,7 @@ #include <vespa/juniper/rpinterface.h> #include <vespa/searchcommon/attribute/iattributecontext.h> #include <vespa/searchlib/common/location.h> +#include "docsum_field_writer_state.h" namespace search { namespace docsummary { @@ -19,6 +20,7 @@ GetDocsumsState::GetDocsumsState(GetDocsumsStateCallback &callback) _docSumFieldSpace(_docSumFieldSpaceStore, sizeof(_docSumFieldSpaceStore)), // only alloc buffer if needed _attrCtx(), _attributes(), + _fieldWriterStates(), _jsonStringer(), _parsedLocation(), _summaryFeatures(NULL), diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.h b/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.h index 4ffed79043e..fa47d5244eb 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.h +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsumstate.h @@ -23,6 +23,7 @@ namespace search::docsummary { class GetDocsumsState; class IDocsumEnvironment; class KeywordExtractor; +class DocsumFieldWriterState; class GetDocsumsStateCallback { @@ -70,6 +71,7 @@ public: char _docSumFieldSpaceStore[2048]; std::unique_ptr<search::attribute::IAttributeContext> _attrCtx; std::vector<const search::attribute::IAttributeVector *> _attributes; + std::vector<std::unique_ptr<DocsumFieldWriterState>> _fieldWriterStates; vespalib::JSONStringer _jsonStringer; // used by AbsDistanceDFW diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.cpp b/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.cpp index bf660b1319b..abd1780b773 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.cpp +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.cpp @@ -2,6 +2,7 @@ #include "docsumwriter.h" #include "docsumstate.h" +#include "docsum_field_writer_state.h" #include <vespa/searchlib/common/transport.h> #include <vespa/searchlib/util/slime_output_raw_buf_adapter.h> #include <vespa/searchlib/attribute/iattributemanager.h> @@ -77,7 +78,6 @@ DynamicDocsumWriter::resolveInputClass(ResolveClassInfo &rci, uint32_t id) const } } - static void convertEntry(GetDocsumsState *state, const ResConfigEntry *resCfg, const ResEntry *entry, @@ -194,6 +194,7 @@ DynamicDocsumWriter::DynamicDocsumWriter( ResultConfig *config, KeywordExtractor _defaultOutputClass(ResultConfig::NoClassID()), _numClasses(config->GetNumResultClasses()), _numEnumValues(config->GetFieldNameEnum().GetNumEntries()), + _numFieldWriterStates(0), _classInfoTable(nullptr), _overrideTable(nullptr) { @@ -267,6 +268,9 @@ DynamicDocsumWriter::Override(const char *fieldName, IDocsumFieldWriter *writer) writer->setIndex(fieldEnumValue); _overrideTable[fieldEnumValue] = writer; + if (writer->setFieldWriterStateIndex(_numFieldWriterStates)) { + ++_numFieldWriterStates; + } for (ResultConfig::iterator it(_resultConfig->begin()), mt(_resultConfig->end()); it != mt; it++) { @@ -288,6 +292,7 @@ DynamicDocsumWriter::InitState(IAttributeManager & attrMan, GetDocsumsState *sta state->_kwExtractor = _keywordExtractor; state->_attrCtx = attrMan.createContext(); state->_attributes.resize(_numEnumValues); + state->_fieldWriterStates.resize(_numFieldWriterStates); for (size_t i(0); i < state->_attributes.size(); i++) { const IDocsumFieldWriter *fw = _overrideTable[i]; if (fw) { diff --git a/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.h b/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.h index 6ef21a71e74..92b26d5cf14 100644 --- a/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.h +++ b/searchsummary/src/vespa/searchsummary/docsummary/docsumwriter.h @@ -54,6 +54,7 @@ private: uint32_t _defaultOutputClass; uint32_t _numClasses; uint32_t _numEnumValues; + uint32_t _numFieldWriterStates; ResultClass::DynamicInfo *_classInfoTable; IDocsumFieldWriter **_overrideTable; diff --git a/service-monitor/pom.xml b/service-monitor/pom.xml index 70f9d4aa655..b8065ed3636 100644 --- a/service-monitor/pom.xml +++ b/service-monitor/pom.xml @@ -64,6 +64,12 @@ <version>${project.version}</version> </dependency> <dependency> + <groupId>com.yahoo.vespa</groupId> + <artifactId>vespa-athenz</artifactId> + <version>${project.version}</version> + <scope>provided</scope> + </dependency> + <dependency> <groupId>com.google.inject</groupId> <artifactId>guice</artifactId> <scope>provided</scope> @@ -76,6 +82,23 @@ <scope>provided</scope> </dependency> <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-core</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.httpcomponents</groupId> + <artifactId>httpclient</artifactId> + <version>4.5</version> + <!-- This is necessary to get 4.4's HostnameVerifier API of SSLConnectionSocketFactory::new --> + <scope>compile</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java index 35003313775..75e61eef772 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/ServiceStatusProvider.java @@ -11,7 +11,13 @@ import com.yahoo.vespa.applicationmodel.ServiceType; * @author hakon */ public interface ServiceStatusProvider { - /** Get the {@link ServiceStatus} of a particular service. */ + /** + * Get the {@link ServiceStatus} of a particular service. + * + * <p>{@link ServiceStatus#NOT_CHECKED NOT_CHECKED} must be returned if the + * service status provider does does not monitor the service status for + * the particular application, cluster, service type, and config id. + */ ServiceStatus getStatus(ApplicationId applicationId, ClusterId clusterId, ServiceType serviceType, diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java index ec2702bcfaf..cbdcce125cc 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGenerator.java @@ -1,13 +1,148 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.application; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.Zone; import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.applicationmodel.ServiceClusterKey; +import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.applicationmodel.TenantId; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.internal.ServiceId; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.yahoo.vespa.service.monitor.application.ConfigServerApplication.CONFIG_SERVER_APPLICATION; /** + * Class to generate an ApplicationInstance given service status for a standard (deployed) application. + * * @author hakon */ -public interface ApplicationInstanceGenerator { - /** Make an ApplicationInstance based on current service status. */ - ApplicationInstance makeApplicationInstance(ServiceStatusProvider serviceStatusProvider); +public class ApplicationInstanceGenerator { + public static final String CLUSTER_ID_PROPERTY_NAME = "clustername"; + + private final ApplicationInfo applicationInfo; + private final Zone zone; + + public ApplicationInstanceGenerator(ApplicationInfo applicationInfo, Zone zone) { + this.applicationInfo = applicationInfo; + this.zone = zone; + } + + public ApplicationInstance makeApplicationInstance(ServiceStatusProvider serviceStatusProvider) { + Map<ServiceClusterKey, Set<ServiceInstance>> groupedServiceInstances = new HashMap<>(); + + for (HostInfo host : applicationInfo.getModel().getHosts()) { + HostName hostName = new HostName(host.getHostname()); + for (ServiceInfo serviceInfo : host.getServices()) { + ServiceClusterKey serviceClusterKey = toServiceClusterKey(serviceInfo); + ServiceInstance serviceInstance = + toServiceInstance( + applicationInfo.getApplicationId(), + serviceClusterKey.clusterId(), + serviceInfo, + hostName, + serviceStatusProvider); + + if (!groupedServiceInstances.containsKey(serviceClusterKey)) { + groupedServiceInstances.put(serviceClusterKey, new HashSet<>()); + } + groupedServiceInstances.get(serviceClusterKey).add(serviceInstance); + } + } + + Set<ServiceCluster> serviceClusters = groupedServiceInstances.entrySet().stream() + .map(entry -> new ServiceCluster( + entry.getKey().clusterId(), + entry.getKey().serviceType(), + entry.getValue())) + .collect(Collectors.toSet()); + + ApplicationInstance applicationInstance = new ApplicationInstance( + new TenantId(applicationInfo.getApplicationId().tenant().toString()), + toApplicationInstanceId(applicationInfo, zone), + serviceClusters); + + // Fill back-references + for (ServiceCluster serviceCluster : applicationInstance.serviceClusters()) { + serviceCluster.setApplicationInstance(applicationInstance); + for (ServiceInstance serviceInstance : serviceCluster.serviceInstances()) { + serviceInstance.setServiceCluster(serviceCluster); + } + } + + return applicationInstance; + } + + private ServiceInstance toServiceInstance( + ApplicationId applicationId, + ClusterId clusterId, + ServiceInfo serviceInfo, + HostName hostName, + ServiceStatusProvider serviceStatusProvider) { + ConfigId configId = toConfigId(serviceInfo); + + ServiceStatus status = serviceStatusProvider.getStatus( + applicationId, + clusterId, + toServiceType(serviceInfo), configId); + + return new ServiceInstance(configId, hostName, status); + } + + private ApplicationInstanceId toApplicationInstanceId(ApplicationInfo applicationInfo, Zone zone) { + if (applicationInfo.getApplicationId().equals(CONFIG_SERVER_APPLICATION.getApplicationId())) { + // Removing this historical discrepancy would break orchestration during rollout. + // An alternative may be to use a feature flag and flip it between releases, + // once that's available. + return new ApplicationInstanceId(applicationInfo.getApplicationId().application().value()); + } else { + return new ApplicationInstanceId(String.format("%s:%s:%s:%s", + applicationInfo.getApplicationId().application().value(), + zone.environment().value(), + zone.region().value(), + applicationInfo.getApplicationId().instance().value())); + } + } + + public static ServiceId getServiceId(ApplicationInfo applicationInfo, ServiceInfo serviceInfo) { + return new ServiceId( + applicationInfo.getApplicationId(), + getClusterId(serviceInfo), + toServiceType(serviceInfo), + toConfigId(serviceInfo)); + } + + private static ClusterId getClusterId(ServiceInfo serviceInfo) { + return new ClusterId(serviceInfo.getProperty(CLUSTER_ID_PROPERTY_NAME).orElse("")); + } + + private static ServiceClusterKey toServiceClusterKey(ServiceInfo serviceInfo) { + ClusterId clusterId = getClusterId(serviceInfo); + ServiceType serviceType = toServiceType(serviceInfo); + return new ServiceClusterKey(clusterId, serviceType); + } + + private static ServiceType toServiceType(ServiceInfo serviceInfo) { + return new ServiceType(serviceInfo.getServiceType()); + } + + private static ConfigId toConfigId(ServiceInfo serviceInfo) { + return new ConfigId(serviceInfo.getConfigId()); + } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGenerator.java deleted file mode 100644 index 76ca59cf583..00000000000 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGenerator.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.service.monitor.application; - -import com.yahoo.vespa.applicationmodel.ApplicationInstance; -import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceCluster; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.applicationmodel.ServiceStatus; -import com.yahoo.vespa.service.monitor.ServiceStatusProvider; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class for generating an ApplicationInstance for the synthesized config server application. - * - * @author hakon - */ -public class ConfigServerAppGenerator implements ApplicationInstanceGenerator { - private final List<String> hostnames; - - public ConfigServerAppGenerator(List<String> hostnames) { - this.hostnames = hostnames; - } - - @Override - public ApplicationInstance makeApplicationInstance(ServiceStatusProvider statusProvider) { - Set<ServiceInstance> serviceInstances = hostnames.stream() - .map(hostname -> makeServiceInstance(hostname, statusProvider)) - .collect(Collectors.toSet()); - - ServiceCluster serviceCluster = new ServiceCluster( - ConfigServerApplication.CLUSTER_ID, - ConfigServerApplication.SERVICE_TYPE, - serviceInstances); - - Set<ServiceCluster> serviceClusters = new HashSet<>(); - serviceClusters.add(serviceCluster); - - ApplicationInstance applicationInstance = new ApplicationInstance( - ConfigServerApplication.TENANT_ID, - ConfigServerApplication.APPLICATION_INSTANCE_ID, - serviceClusters); - - // Fill back-references - serviceCluster.setApplicationInstance(applicationInstance); - for (ServiceInstance serviceInstance : serviceCluster.serviceInstances()) { - serviceInstance.setServiceCluster(serviceCluster); - } - - return applicationInstance; - } - - private ServiceInstance makeServiceInstance(String hostname, ServiceStatusProvider statusProvider) { - ConfigId configId = new ConfigId(ConfigServerApplication.CONFIG_ID_PREFIX + hostname); - ServiceStatus status = statusProvider.getStatus( - ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId(), - ConfigServerApplication.CLUSTER_ID, - ConfigServerApplication.SERVICE_TYPE, - configId); - - return new ServiceInstance(configId, new HostName(hostname), status); - } -} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java index 132bb0927b8..5ad38cebcfc 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ConfigServerApplication.java @@ -1,12 +1,26 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.application; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.PortInfo; +import com.yahoo.config.model.api.ServiceInfo; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.NodeType; import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.service.monitor.internal.ModelGenerator; +import com.yahoo.vespa.service.monitor.internal.health.ApplicationHealthMonitor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * A service/application model of the config server with health status. @@ -21,8 +35,44 @@ public class ConfigServerApplication extends HostedVespaApplication { public static final ServiceType SERVICE_TYPE = new ServiceType("configserver"); public static final String CONFIG_ID_PREFIX = "configid."; + public static ConfigId configIdFrom(int index) { + return new ConfigId(CONFIG_ID_PREFIX + index); + } + private ConfigServerApplication() { super("zone-config-servers", NodeType.config, ClusterSpec.Type.admin, ClusterSpec.Id.from("zone-config-servers")); } + + public ApplicationInfo makeApplicationInfo(ConfigserverConfig config) { + List<HostInfo> hostInfos = new ArrayList<>(); + List<ConfigserverConfig.Zookeeperserver> zooKeeperServers = config.zookeeperserver(); + for (int index = 0; index < zooKeeperServers.size(); ++index) { + String hostname = zooKeeperServers.get(index).hostname(); + hostInfos.add(makeHostInfo(hostname, config.httpport(), index)); + } + + return new ApplicationInfo( + CONFIG_SERVER_APPLICATION.getApplicationId(), + 0, + new HostsModel(hostInfos)); + } + + private static HostInfo makeHostInfo(String hostname, int port, int configIndex) { + PortInfo portInfo = new PortInfo(port, ApplicationHealthMonitor.PORT_TAGS_HEALTH); + + Map<String, String> properties = new HashMap<>(); + properties.put(ModelGenerator.CLUSTER_ID_PROPERTY_NAME, CLUSTER_ID.s()); + + ServiceInfo serviceInfo = new ServiceInfo( + // service name == service type for the first service of each type on each host + SERVICE_TYPE.s(), + SERVICE_TYPE.s(), + Collections.singletonList(portInfo), + properties, + configIdFrom(configIndex).s(), + hostname); + + return new HostInfo(hostname, Collections.singletonList(serviceInfo)); + } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/DeployedAppGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/DeployedAppGenerator.java deleted file mode 100644 index 2691a8bf1ee..00000000000 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/DeployedAppGenerator.java +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.service.monitor.application; - -import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.HostInfo; -import com.yahoo.config.model.api.ServiceInfo; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.Zone; -import com.yahoo.vespa.applicationmodel.ApplicationInstance; -import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; -import com.yahoo.vespa.applicationmodel.ClusterId; -import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceCluster; -import com.yahoo.vespa.applicationmodel.ServiceClusterKey; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.applicationmodel.ServiceStatus; -import com.yahoo.vespa.applicationmodel.ServiceType; -import com.yahoo.vespa.applicationmodel.TenantId; -import com.yahoo.vespa.service.monitor.ServiceStatusProvider; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class to generate an ApplicationInstance given service status for a standard (deployed) application. - * - * @author hakon - */ -public class DeployedAppGenerator implements ApplicationInstanceGenerator { - public static final String CLUSTER_ID_PROPERTY_NAME = "clustername"; - - private final ApplicationInfo applicationInfo; - private final Zone zone; - - public DeployedAppGenerator(ApplicationInfo applicationInfo, Zone zone) { - this.applicationInfo = applicationInfo; - this.zone = zone; - } - - @Override - public ApplicationInstance makeApplicationInstance(ServiceStatusProvider serviceStatusProvider) { - Map<ServiceClusterKey, Set<ServiceInstance>> groupedServiceInstances = new HashMap<>(); - - for (HostInfo host : applicationInfo.getModel().getHosts()) { - HostName hostName = new HostName(host.getHostname()); - for (ServiceInfo serviceInfo : host.getServices()) { - ServiceClusterKey serviceClusterKey = toServiceClusterKey(serviceInfo); - ServiceInstance serviceInstance = - toServiceInstance( - applicationInfo.getApplicationId(), - serviceClusterKey.clusterId(), - serviceInfo, - hostName, - serviceStatusProvider); - - if (!groupedServiceInstances.containsKey(serviceClusterKey)) { - groupedServiceInstances.put(serviceClusterKey, new HashSet<>()); - } - groupedServiceInstances.get(serviceClusterKey).add(serviceInstance); - } - } - - Set<ServiceCluster> serviceClusters = groupedServiceInstances.entrySet().stream() - .map(entry -> new ServiceCluster( - entry.getKey().clusterId(), - entry.getKey().serviceType(), - entry.getValue())) - .collect(Collectors.toSet()); - - ApplicationInstance applicationInstance = new ApplicationInstance( - new TenantId(applicationInfo.getApplicationId().tenant().toString()), - toApplicationInstanceId(applicationInfo, zone), - serviceClusters); - - // Fill back-references - for (ServiceCluster serviceCluster : applicationInstance.serviceClusters()) { - serviceCluster.setApplicationInstance(applicationInstance); - for (ServiceInstance serviceInstance : serviceCluster.serviceInstances()) { - serviceInstance.setServiceCluster(serviceCluster); - } - } - - return applicationInstance; - } - - static ClusterId getClusterId(ServiceInfo serviceInfo) { - return new ClusterId(serviceInfo.getProperty(CLUSTER_ID_PROPERTY_NAME).orElse("")); - } - - private ServiceClusterKey toServiceClusterKey(ServiceInfo serviceInfo) { - ClusterId clusterId = getClusterId(serviceInfo); - ServiceType serviceType = toServiceType(serviceInfo); - return new ServiceClusterKey(clusterId, serviceType); - } - - private ServiceInstance toServiceInstance( - ApplicationId applicationId, - ClusterId clusterId, - ServiceInfo serviceInfo, - HostName hostName, - ServiceStatusProvider serviceStatusProvider) { - ConfigId configId = new ConfigId(serviceInfo.getConfigId()); - - ServiceStatus status = serviceStatusProvider.getStatus( - applicationId, - clusterId, - toServiceType(serviceInfo), configId); - - return new ServiceInstance(configId, hostName, status); - } - - private ApplicationInstanceId toApplicationInstanceId(ApplicationInfo applicationInfo, Zone zone) { - return new ApplicationInstanceId(String.format("%s:%s:%s:%s", - applicationInfo.getApplicationId().application().value(), - zone.environment().value(), - zone.region().value(), - applicationInfo.getApplicationId().instance().value())); - } - - private ServiceType toServiceType(ServiceInfo serviceInfo) { - return new ServiceType(serviceInfo.getServiceType()); - } -} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/HostsModel.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/HostsModel.java new file mode 100644 index 00000000000..225ffb0adbc --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/HostsModel.java @@ -0,0 +1,75 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.application; + +import com.yahoo.config.FileReference; +import com.yahoo.config.model.api.FileDistribution; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.Model; +import com.yahoo.config.provision.AllocatedHosts; +import com.yahoo.vespa.config.ConfigKey; +import com.yahoo.vespa.config.ConfigPayload; +import com.yahoo.vespa.config.buildergen.ConfigDefinition; + +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Model that only supports the subset necessary to create an ApplicationInstance. + * + * @author hakon + */ +public class HostsModel implements Model { + private final Collection<HostInfo> hosts; + + public HostsModel(List<HostInfo> hosts) { + this.hosts = Collections.unmodifiableCollection(hosts); + } + + @Override + public Collection<HostInfo> getHosts() { + return hosts; + } + + @Override + public ConfigPayload getConfig(ConfigKey<?> configKey, ConfigDefinition configDefinition) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<ConfigKey<?>> allConfigsProduced() { + throw new UnsupportedOperationException(); + } + + @Override + public Set<String> allConfigIds() { + throw new UnsupportedOperationException(); + } + + @Override + public void distributeFiles(FileDistribution fileDistribution) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<FileReference> fileReferences() { + throw new UnsupportedOperationException(); + } + + @Override + public AllocatedHosts allocatedHosts() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean allowModelVersionMismatch(Instant now) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean skipOldConfigModels(Instant now) { + throw new UnsupportedOperationException(); + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java index 6bbf0cb6d1d..c10015d3bfa 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/application/ZoneApplication.java @@ -21,8 +21,8 @@ public class ZoneApplication { .createHostedVespaApplicationId("routing"); public static boolean isNodeAdminService(ApplicationId applicationId, - ClusterId clusterId, - ServiceType serviceType) { + ClusterId clusterId, + ServiceType serviceType) { return Objects.equals(applicationId, ZONE_APPLICATION_ID) && Objects.equals(serviceType, ServiceType.CONTAINER) && Objects.equals(clusterId, ClusterId.NODE_ADMIN); diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModel.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModel.java new file mode 100644 index 00000000000..80e0bfd2710 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModel.java @@ -0,0 +1,42 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.SuperModel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static com.yahoo.vespa.service.monitor.application.ConfigServerApplication.CONFIG_SERVER_APPLICATION; + +/** + * The {@code DuperModel} unites the {@link com.yahoo.config.model.api.SuperModel SuperModel} + * with the synthetically produced applications like the config server application. + * + * @author hakon + */ +public class DuperModel { + private final List<ApplicationInfo> staticApplicationInfos = new ArrayList<>(); + + public DuperModel(ConfigserverConfig configServerConfig) { + // Single-tenant applications have the config server as part of the application model. + // TODO: Add health monitoring for config server when part of application model. + if (configServerConfig.multitenant()) { + staticApplicationInfos.add(CONFIG_SERVER_APPLICATION.makeApplicationInfo(configServerConfig)); + } + } + + /** For testing. */ + DuperModel(ApplicationInfo... staticApplicationInfos) { + this.staticApplicationInfos.addAll(Arrays.asList(staticApplicationInfos)); + } + + public List<ApplicationInfo> getApplicationInfos(SuperModel superModelSnapshot) { + List<ApplicationInfo> allApplicationInfos = new ArrayList<>(); + allApplicationInfos.addAll(staticApplicationInfos); + allApplicationInfos.addAll(superModelSnapshot.getAllApplicationInfos()); + return allApplicationInfos; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModelListener.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModelListener.java new file mode 100644 index 00000000000..235c7db5c36 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/DuperModelListener.java @@ -0,0 +1,28 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.provision.ApplicationId; + +/** + * Interface for listening for changes to the {@link DuperModel}. + * + * @author hakon + */ +public interface DuperModelListener { + /** + * An application has been activated: + * + * <ul> + * <li>A synthetic application like the config server application has been added/"activated" + * <li>A super model application has been activated (see + * {@link com.yahoo.config.model.api.SuperModelListener#applicationActivated(SuperModel, ApplicationInfo) + * SuperModelListener} + * </ul> + */ + void applicationActivated(ApplicationInfo application); + + /** Application has been removed. */ + void applicationRemoved(ApplicationId id); +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java index 9da449289a7..ad2f223acf8 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ModelGenerator.java @@ -1,56 +1,40 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.model.api.ApplicationInfo; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; import com.yahoo.vespa.service.monitor.ServiceModel; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; import com.yahoo.vespa.service.monitor.application.ApplicationInstanceGenerator; -import com.yahoo.vespa.service.monitor.application.ConfigServerAppGenerator; -import com.yahoo.vespa.service.monitor.application.DeployedAppGenerator; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; /** - * Util to convert SuperModel to ServiceModel and application model classes + * Util to make ServiceModel and its related application model classes */ public class ModelGenerator { public static final String CLUSTER_ID_PROPERTY_NAME = "clustername"; - private final List<ApplicationInstanceGenerator> staticGenerators; - - public ModelGenerator(List<String> configServerHosts) { - if (configServerHosts.isEmpty()) { - staticGenerators = Collections.emptyList(); - } else { - staticGenerators = Collections.singletonList(new ConfigServerAppGenerator(configServerHosts)); - } - } - /** * Create service model based primarily on super model. * * If the configServerhosts is non-empty, a config server application is added. */ - ServiceModel toServiceModel( - SuperModel superModel, - Zone zone, - ServiceStatusProvider serviceStatusProvider) { - List<ApplicationInstanceGenerator> generators = new ArrayList<>(staticGenerators); - superModel.getAllApplicationInfos() - .forEach(info -> generators.add(new DeployedAppGenerator(info, zone))); - - Map<ApplicationInstanceReference, ApplicationInstance> applicationInstances = generators.stream() - .map(generator -> generator.makeApplicationInstance(serviceStatusProvider)) - .collect(Collectors.toMap(ApplicationInstance::reference, Function.identity())); + public ServiceModel toServiceModel(List<ApplicationInfo> allApplicationInfos, + Zone zone, + ServiceStatusProvider serviceStatusProvider) { + Map<ApplicationInstanceReference, ApplicationInstance> applicationInstances = + allApplicationInfos.stream() + .map(info -> new ApplicationInstanceGenerator(info, zone) + .makeApplicationInstance(serviceStatusProvider)) + .collect(Collectors.toMap(ApplicationInstance::reference, Function.identity())); return new ServiceModel(applicationInstances); } + } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java index 49863672c43..1edf3a18215 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/MonitorManager.java @@ -1,11 +1,10 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.service.monitor.internal;// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.config.model.api.SuperModelListener; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; /** * @author hakon */ -public interface MonitorManager extends SuperModelListener, ServiceStatusProvider { +public interface MonitorManager extends DuperModelListener, ServiceStatusProvider { } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceId.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceId.java new file mode 100644 index 00000000000..993ea7fed5c --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceId.java @@ -0,0 +1,75 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceType; + +import javax.annotation.concurrent.Immutable; +import java.util.Objects; + +/** + * Identifies a service. + * + * @author hakon + */ +@Immutable +public class ServiceId { + private final ApplicationId applicationId; + private final ClusterId clusterId; + private final ServiceType serviceType; + private final ConfigId configId; + + public ServiceId(ApplicationId applicationId, + ClusterId clusterId, + ServiceType serviceType, + ConfigId configId) { + this.applicationId = applicationId; + this.clusterId = clusterId; + this.serviceType = serviceType; + this.configId = configId; + } + + public ApplicationId getApplicationId() { + return applicationId; + } + + public ClusterId getClusterId() { + return clusterId; + } + + public ServiceType getServiceType() { + return serviceType; + } + + public ConfigId getConfigId() { + return configId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServiceId serviceId = (ServiceId) o; + return Objects.equals(applicationId, serviceId.applicationId) && + Objects.equals(clusterId, serviceId.clusterId) && + Objects.equals(serviceType, serviceId.serviceType) && + Objects.equals(configId, serviceId.configId); + } + + @Override + public int hashCode() { + return Objects.hash(applicationId, clusterId, serviceType, configId); + } + + @Override + public String toString() { + return "ServiceId{" + + "applicationId=" + applicationId + + ", clusterId=" + clusterId + + ", serviceType=" + serviceType + + ", configId=" + configId + + '}'; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java index 97c4fdda0f3..bd8fd4a50e0 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/ServiceMonitorImpl.java @@ -14,10 +14,7 @@ import com.yahoo.vespa.service.monitor.ServiceMonitor; import com.yahoo.vespa.service.monitor.internal.health.HealthMonitorManager; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; -import java.util.Collections; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class ServiceMonitorImpl implements ServiceMonitor { private final ServiceModelCache serviceModelCache; @@ -32,30 +29,20 @@ public class ServiceMonitorImpl implements ServiceMonitor { Zone zone = superModelProvider.getZone(); ServiceMonitorMetrics metrics = new ServiceMonitorMetrics(metric, timer); - UnionMonitorManager monitorManager = new UnionMonitorManager( - slobrokMonitorManager, - healthMonitorManager, - configserverConfig); + DuperModel duperModel = new DuperModel(configserverConfig); + UnionMonitorManager monitorManager = + new UnionMonitorManager(slobrokMonitorManager, healthMonitorManager); SuperModelListenerImpl superModelListener = new SuperModelListenerImpl( monitorManager, metrics, - new ModelGenerator(toConfigServerList(configserverConfig)), + duperModel, + new ModelGenerator(), zone); superModelListener.start(superModelProvider); serviceModelCache = new ServiceModelCache(superModelListener, timer); } - private List<String> toConfigServerList(ConfigserverConfig configserverConfig) { - if (configserverConfig.multitenant()) { - return configserverConfig.zookeeperserver().stream() - .map(ConfigserverConfig.Zookeeperserver::hostname) - .collect(Collectors.toList()); - } - - return Collections.emptyList(); - } - @Override public Map<ApplicationInstanceReference, ApplicationInstance> getAllApplicationInstances() { return serviceModelCache.get().getAllApplicationInstances(); diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java index b2f3617131b..f509809c33d 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImpl.java @@ -8,7 +8,9 @@ import com.yahoo.config.model.api.SuperModelProvider; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.service.monitor.ServiceModel; +import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import java.util.List; import java.util.function.Supplier; import java.util.logging.Logger; @@ -16,6 +18,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv private static final Logger logger = Logger.getLogger(SuperModelListenerImpl.class.getName()); private final ServiceMonitorMetrics metrics; + private final DuperModel duperModel; private final ModelGenerator modelGenerator; private final Zone zone; @@ -27,10 +30,12 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv SuperModelListenerImpl(MonitorManager monitorManager, ServiceMonitorMetrics metrics, + DuperModel duperModel, ModelGenerator modelGenerator, Zone zone) { this.monitorManager = monitorManager; this.metrics = metrics; + this.duperModel = duperModel; this.modelGenerator = modelGenerator; this.zone = zone; } @@ -41,8 +46,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv // since applicationActivated()/applicationRemoved() may be called // asynchronously even before snapshot() returns. this.superModel = superModelProvider.snapshot(this); - superModel.getAllApplicationInfos().stream().forEach(application -> - monitorManager.applicationActivated(superModel, application)); + duperModel.getApplicationInfos(superModel).forEach(monitorManager::applicationActivated); } } @@ -50,7 +54,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv public void applicationActivated(SuperModel superModel, ApplicationInfo application) { synchronized (monitor) { this.superModel = superModel; - monitorManager.applicationActivated(superModel, application); + monitorManager.applicationActivated(application); } } @@ -58,7 +62,7 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv public void applicationRemoved(SuperModel superModel, ApplicationId id) { synchronized (monitor) { this.superModel = superModel; - monitorManager.applicationRemoved(superModel, id); + monitorManager.applicationRemoved(id); } } @@ -71,7 +75,9 @@ public class SuperModelListenerImpl implements SuperModelListener, Supplier<Serv dummy(measurement); // WARNING: The slobrok monitor manager may be out-of-sync with super model (no locking) - return modelGenerator.toServiceModel(superModel, zone, monitorManager); + List<ApplicationInfo> applicationInfos = duperModel.getApplicationInfos(superModel); + + return modelGenerator.toServiceModel(applicationInfos, zone, (ServiceStatusProvider) monitorManager); } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java index 82d2043bd17..81cf6f2af5e 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManager.java @@ -1,16 +1,12 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.applicationmodel.ServiceType; -import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; -import com.yahoo.vespa.service.monitor.application.ZoneApplication; import com.yahoo.vespa.service.monitor.internal.health.HealthMonitorManager; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; @@ -20,14 +16,11 @@ import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImp public class UnionMonitorManager implements MonitorManager { private final SlobrokMonitorManagerImpl slobrokMonitorManager; private final HealthMonitorManager healthMonitorManager; - private final ConfigserverConfig configserverConfig; UnionMonitorManager(SlobrokMonitorManagerImpl slobrokMonitorManager, - HealthMonitorManager healthMonitorManager, - ConfigserverConfig configserverConfig) { + HealthMonitorManager healthMonitorManager) { this.slobrokMonitorManager = slobrokMonitorManager; this.healthMonitorManager = healthMonitorManager; - this.configserverConfig = configserverConfig; } @Override @@ -35,33 +28,25 @@ public class UnionMonitorManager implements MonitorManager { ClusterId clusterId, ServiceType serviceType, ConfigId configId) { - - if (applicationId.equals(ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId())) { - // todo: use health - return ServiceStatus.NOT_CHECKED; + // Trust the new health monitoring status if it actually monitors the particular service. + ServiceStatus status = healthMonitorManager.getStatus(applicationId, clusterId, serviceType, configId); + if (status != ServiceStatus.NOT_CHECKED) { + return status; } - MonitorManager monitorManager = useHealth(applicationId, clusterId, serviceType) ? - healthMonitorManager : - slobrokMonitorManager; - - return monitorManager.getStatus(applicationId, clusterId, serviceType, configId); + // fallback is the older slobrok + return slobrokMonitorManager.getStatus(applicationId, clusterId, serviceType, configId); } @Override - public void applicationActivated(SuperModel superModel, ApplicationInfo application) { - slobrokMonitorManager.applicationActivated(superModel, application); - healthMonitorManager.applicationActivated(superModel, application); + public void applicationActivated(ApplicationInfo application) { + slobrokMonitorManager.applicationActivated(application); + healthMonitorManager.applicationActivated(application); } @Override - public void applicationRemoved(SuperModel superModel, ApplicationId id) { - slobrokMonitorManager.applicationRemoved(superModel, id); - healthMonitorManager.applicationRemoved(superModel, id); - } - - private boolean useHealth(ApplicationId applicationId, ClusterId clusterId, ServiceType serviceType) { - return !configserverConfig.nodeAdminInContainer() && - ZoneApplication.isNodeAdminService(applicationId, clusterId, serviceType); + public void applicationRemoved(ApplicationId id) { + slobrokMonitorManager.applicationRemoved(id); + healthMonitorManager.applicationRemoved(id); } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitor.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitor.java new file mode 100644 index 00000000000..bd2658db8aa --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitor.java @@ -0,0 +1,102 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.HostInfo; +import com.yahoo.config.model.api.PortInfo; +import com.yahoo.config.model.api.ServiceInfo; +import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.application.ApplicationInstanceGenerator; +import com.yahoo.vespa.service.monitor.internal.ServiceId; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Responsible for monitoring a whole application using /state/v1/health. + * + * @author hakon + */ +public class ApplicationHealthMonitor implements ServiceStatusProvider, AutoCloseable { + public static final String PORT_TAG_STATE = "STATE"; + public static final String PORT_TAG_HTTP = "HTTP"; + /** Port tags implying /state/v1/health is served */ + public static final List<String> PORT_TAGS_HEALTH = + Collections.unmodifiableList(Arrays.asList(PORT_TAG_HTTP, PORT_TAG_STATE)); + + private final Map<ServiceId, HealthMonitor> healthMonitors; + + public static ApplicationHealthMonitor startMonitoring(ApplicationInfo application) { + return new ApplicationHealthMonitor(makeHealthMonitors(application)); + } + + private ApplicationHealthMonitor(Map<ServiceId, HealthMonitor> healthMonitors) { + this.healthMonitors = healthMonitors; + } + + @Override + public ServiceStatus getStatus(ApplicationId applicationId, + ClusterId clusterId, + ServiceType serviceType, + ConfigId configId) { + ServiceId serviceId = new ServiceId(applicationId, clusterId, serviceType, configId); + HealthMonitor monitor = healthMonitors.get(serviceId); + if (monitor == null) { + return ServiceStatus.NOT_CHECKED; + } + + return monitor.getStatus(); + } + + @Override + public void close() { + healthMonitors.values().forEach(HealthMonitor::close); + healthMonitors.clear(); + } + + private static Map<ServiceId, HealthMonitor> makeHealthMonitors(ApplicationInfo application) { + Map<ServiceId, HealthMonitor> healthMonitors = new HashMap<>(); + for (HostInfo hostInfo : application.getModel().getHosts()) { + for (ServiceInfo serviceInfo : hostInfo.getServices()) { + for (PortInfo portInfo : serviceInfo.getPorts()) { + maybeCreateHealthMonitor( + application, + hostInfo, + serviceInfo, + portInfo) + .ifPresent(healthMonitor -> healthMonitors.put( + ApplicationInstanceGenerator.getServiceId(application, serviceInfo), + healthMonitor)); + } + } + } + return healthMonitors; + } + + private static Optional<HealthMonitor> maybeCreateHealthMonitor( + ApplicationInfo applicationInfo, + HostInfo hostInfo, + ServiceInfo serviceInfo, + PortInfo portInfo) { + if (portInfo.getTags().containsAll(PORT_TAGS_HEALTH)) { + HostName hostname = HostName.from(hostInfo.getHostname()); + HealthEndpoint endpoint = HealthEndpoint.forHttp(hostname, portInfo.getPort()); + // todo: make HealthMonitor + // HealthMonitor healthMonitor = new HealthMonitor(endpoint); + // healthMonitor.startMonitoring(); + return Optional.empty(); + } + + return Optional.empty(); + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthClient.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthClient.java new file mode 100644 index 00000000000..43a02a385be --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthClient.java @@ -0,0 +1,139 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yahoo.vespa.athenz.api.AthenzService; +import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.ConnectionKeepAliveStrategy; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; + +import javax.net.ssl.SSLContext; + +/** + * @author hakon + */ +public class HealthClient implements AutoCloseable, ServiceIdentityProvider.Listener { + private static final ObjectMapper mapper = new ObjectMapper(); + private static final long MAX_CONTENT_LENGTH = 1L << 20; // 1 MB + private static final int DEFAULT_TIMEOUT_MILLIS = 1_000; + + private static final ConnectionKeepAliveStrategy KEEP_ALIVE_STRATEGY = + new DefaultConnectionKeepAliveStrategy() { + @Override + public long getKeepAliveDuration(HttpResponse response, HttpContext context) { + long keepAlive = super.getKeepAliveDuration(response, context); + if (keepAlive == -1) { + // Keep connections alive 60 seconds if a keep-alive value + // has not be explicitly set by the server + keepAlive = 60000; + } + return keepAlive; + } + }; + + private final HealthEndpoint endpoint; + + private volatile CloseableHttpClient httpClient; + + public HealthClient(HealthEndpoint endpoint) { + this.endpoint = endpoint; + } + + public void start() { + endpoint.getServiceIdentityProvider().ifPresent(provider -> { + onCredentialsUpdate(provider.getIdentitySslContext(), null); + provider.addIdentityListener(this); + }); + } + + @Override + public void onCredentialsUpdate(SSLContext sslContext, AthenzService ignored) { + SSLConnectionSocketFactory socketFactory = + new SSLConnectionSocketFactory(sslContext, endpoint.getHostnameVerifier().orElse(null)); + + Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() + .register("https", socketFactory) + .build(); + + HttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(registry); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(DEFAULT_TIMEOUT_MILLIS) // establishment of connection + .setConnectionRequestTimeout(DEFAULT_TIMEOUT_MILLIS) // connection from connection manager + .setSocketTimeout(DEFAULT_TIMEOUT_MILLIS) // waiting for data + .build(); + + this.httpClient = HttpClients.custom() + .setKeepAliveStrategy(KEEP_ALIVE_STRATEGY) + .setConnectionManager(connectionManager) + .disableAutomaticRetries() + .setDefaultRequestConfig(requestConfig) + .build(); + } + + public HealthInfo getHealthInfo() { + try { + return probeHealth(); + } catch (Exception e) { + return HealthInfo.fromException(e); + } + } + + @Override + public void close() { + endpoint.getServiceIdentityProvider().ifPresent(provider -> provider.removeIdentityListener(this)); + + try { + httpClient.close(); + } catch (Exception e) { + // ignore + } + httpClient = null; + } + + private HealthInfo probeHealth() throws Exception { + HttpGet httpget = new HttpGet(endpoint.getStateV1HealthUrl().toString()); + CloseableHttpResponse httpResponse; + + CloseableHttpClient httpClient = this.httpClient; + if (httpClient == null) { + throw new IllegalStateException("HTTP client has closed"); + } + + httpResponse = httpClient.execute(httpget); + + int httpStatusCode = httpResponse.getStatusLine().getStatusCode(); + if (httpStatusCode < 200 || httpStatusCode >= 300) { + return HealthInfo.fromBadHttpStatusCode(httpStatusCode); + } + + HttpEntity bodyEntity = httpResponse.getEntity(); + long contentLength = bodyEntity.getContentLength(); + if (contentLength > MAX_CONTENT_LENGTH) { + throw new IllegalArgumentException("Content too long: " + contentLength + " bytes"); + } + String body = EntityUtils.toString(bodyEntity); + HealthResponse healthResponse = mapper.readValue(body, HealthResponse.class); + + if (healthResponse.status == null || healthResponse.status.code == null) { + return HealthInfo.fromHealthStatusCode(HealthResponse.Status.DEFAULT_STATUS); + } else { + return HealthInfo.fromHealthStatusCode(healthResponse.status.code); + } + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthEndpoint.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthEndpoint.java new file mode 100644 index 00000000000..e9d17a9ab70 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthEndpoint.java @@ -0,0 +1,57 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.config.provision.HostName; +import com.yahoo.vespa.athenz.api.AthenzIdentity; +import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider; +import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier; + +import javax.net.ssl.HostnameVerifier; +import java.net.URL; +import java.util.Collections; +import java.util.Optional; + +import static com.yahoo.yolean.Exceptions.uncheck; + +/** + * @author hakon + */ +class HealthEndpoint { + private final URL url; + private final Optional<HostnameVerifier> hostnameVerifier; + private final Optional<ServiceIdentityProvider> serviceIdentityProvider; + + static HealthEndpoint forHttp(HostName hostname, int port) { + URL url = uncheck(() -> new URL("http", hostname.value(), port, "/state/v1/health")); + return new HealthEndpoint(url, Optional.empty(), Optional.empty()); + } + + static HealthEndpoint forHttps(HostName hostname, + int port, + ServiceIdentityProvider serviceIdentityProvider, + AthenzIdentity remoteIdentity) { + URL url = uncheck(() -> new URL("https", hostname.value(), port, "/state/v1/health")); + HostnameVerifier peerVerifier = new AthenzIdentityVerifier(Collections.singleton(remoteIdentity)); + return new HealthEndpoint(url, Optional.of(serviceIdentityProvider), Optional.of(peerVerifier)); + } + + private HealthEndpoint(URL url, + Optional<ServiceIdentityProvider> serviceIdentityProvider, + Optional<HostnameVerifier> hostnameVerifier) { + this.url = url; + this.serviceIdentityProvider = serviceIdentityProvider; + this.hostnameVerifier = hostnameVerifier; + } + + public URL getStateV1HealthUrl() { + return url; + } + + public Optional<ServiceIdentityProvider> getServiceIdentityProvider() { + return serviceIdentityProvider; + } + + public Optional<HostnameVerifier> getHostnameVerifier() { + return hostnameVerifier; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthInfo.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthInfo.java new file mode 100644 index 00000000000..a3fe3cb3106 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthInfo.java @@ -0,0 +1,75 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.yolean.Exceptions; + +import java.time.Instant; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * The result of a health lookup. + * + * @author hakon + */ +public class HealthInfo { + public static final String UP_STATUS_CODE = "up"; + + private final Optional<Exception> exception; + private final OptionalInt httpStatusCode; + private final Optional<String> healthStatusCode; + private final Instant time; + + static HealthInfo fromException(Exception exception) { + return new HealthInfo(Optional.of(exception), OptionalInt.empty(), Optional.empty()); + } + + static HealthInfo fromBadHttpStatusCode(int httpStatusCode) { + return new HealthInfo(Optional.empty(), OptionalInt.of(httpStatusCode), Optional.empty()); + } + + static HealthInfo fromHealthStatusCode(String healthStatusCode) { + return new HealthInfo(Optional.empty(), OptionalInt.empty(), Optional.of(healthStatusCode)); + } + + static HealthInfo empty() { + return new HealthInfo(Optional.empty(), OptionalInt.empty(), Optional.empty()); + } + + private HealthInfo(Optional<Exception> exception, + OptionalInt httpStatusCode, + Optional<String> healthStatusCode) { + this.exception = exception; + this.httpStatusCode = httpStatusCode; + this.healthStatusCode = healthStatusCode; + this.time = Instant.now(); + } + + public boolean isHealthy() { + return healthStatusCode.map(UP_STATUS_CODE::equals).orElse(false); + } + + public ServiceStatus toSerivceStatus() { + return isHealthy() ? ServiceStatus.UP : ServiceStatus.DOWN; + } + + public Instant time() { + return time; + } + + @Override + public String toString() { + if (isHealthy()) { + return UP_STATUS_CODE; + } else if (healthStatusCode.isPresent()) { + return "Bad health status code '" + healthStatusCode.get() + "'"; + } else if (exception.isPresent()) { + return Exceptions.toMessageString(exception.get()); + } else if (httpStatusCode.isPresent()) { + return "Bad HTTP response status code " + httpStatusCode.getAsInt(); + } else { + return "No health info available"; + } + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitor.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitor.java new file mode 100644 index 00000000000..fd809b32918 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitor.java @@ -0,0 +1,73 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.applicationmodel.ServiceStatus; + +import java.time.Duration; +import java.util.Random; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Used to monitor the health of a single URL endpoint. + * + * @author hakon + */ +public class HealthMonitor implements AutoCloseable { + private static final Logger logger = Logger.getLogger(HealthMonitor.class.getName()); + private static final Duration DELAY = Duration.ofSeconds(20); + // About 'static': Javadoc says "Instances of java.util.Random are threadsafe." + private static final Random random = new Random(); + + private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1); + private final HealthClient healthClient; + + private volatile HealthInfo lastHealthInfo = HealthInfo.empty(); + + public HealthMonitor(HealthEndpoint stateV1HealthEndpoint) { + this.healthClient = new HealthClient(stateV1HealthEndpoint); + } + + /** For testing. */ + HealthMonitor(HealthClient healthClient) { + this.healthClient = healthClient; + } + + public void startMonitoring() { + healthClient.start(); + executor.scheduleWithFixedDelay( + this::updateSynchronously, + initialDelayInSeconds(DELAY.getSeconds()), + DELAY.getSeconds(), + TimeUnit.SECONDS); + } + + public ServiceStatus getStatus() { + // todo: return lastHealthInfo.toServiceStatus(); + return ServiceStatus.NOT_CHECKED; + } + + @Override + public void close() { + executor.shutdown(); + + try { + executor.awaitTermination(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.log(LogLevel.INFO, "Interrupted while waiting for health monitor termination: " + + e.getMessage()); + } + + healthClient.close(); + } + + private long initialDelayInSeconds(long maxInitialDelayInSeconds) { + return random.nextLong() % maxInitialDelayInSeconds; + } + + private void updateSynchronously() { + lastHealthInfo = healthClient.getHealthInfo(); + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java index 5a4b7251ae2..473ef5e3a94 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManager.java @@ -2,8 +2,8 @@ package com.yahoo.vespa.service.monitor.internal.health; import com.google.inject.Inject; +import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; @@ -12,19 +12,38 @@ import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.service.monitor.application.ZoneApplication; import com.yahoo.vespa.service.monitor.internal.MonitorManager; +import java.util.HashMap; +import java.util.Map; + /** * @author hakon */ public class HealthMonitorManager implements MonitorManager { + private final Map<ApplicationId, ApplicationHealthMonitor> healthMonitors = new HashMap<>(); + private final ConfigserverConfig configserverConfig; + @Inject - public HealthMonitorManager() {} + public HealthMonitorManager(ConfigserverConfig configserverConfig) { + this.configserverConfig = configserverConfig; + } @Override - public void applicationActivated(SuperModel superModel, ApplicationInfo application) { + public void applicationActivated(ApplicationInfo application) { + if (applicationMonitored(application.getApplicationId())) { + ApplicationHealthMonitor monitor = + ApplicationHealthMonitor.startMonitoring(application); + healthMonitors.put(application.getApplicationId(), monitor); + } } @Override - public void applicationRemoved(SuperModel superModel, ApplicationId id) { + public void applicationRemoved(ApplicationId id) { + if (applicationMonitored(id)) { + ApplicationHealthMonitor monitor = healthMonitors.remove(id); + if (monitor != null) { + monitor.close(); + } + } } @Override @@ -32,13 +51,18 @@ public class HealthMonitorManager implements MonitorManager { ClusterId clusterId, ServiceType serviceType, ConfigId configId) { - // TODO: Do proper health check - if (ZoneApplication.isNodeAdminService(applicationId, clusterId, serviceType)) { + if (!configserverConfig.nodeAdminInContainer() && + ZoneApplication.isNodeAdminService(applicationId, clusterId, serviceType)) { + // If node admin doesn't run in a JDisc container, it must be monitored with health. + // TODO: Do proper health check return ServiceStatus.UP; } - throw new IllegalArgumentException("Health monitoring not implemented for application " + - applicationId.toShortString() + ", cluster " + clusterId.s() + ", serviceType " + - serviceType); + return ServiceStatus.NOT_CHECKED; + } + + private boolean applicationMonitored(ApplicationId id) { + // todo: health-check config server + return false; } } diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthResponse.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthResponse.java new file mode 100644 index 00000000000..574523ad564 --- /dev/null +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/health/HealthResponse.java @@ -0,0 +1,35 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yahoo.text.JSON; + +/** + * Response entity from /state/v1/health + * + * @author hakon + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class HealthResponse { + @JsonProperty("status") + public Status status = new Status(); + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Status { + public static final String DEFAULT_STATUS = "down"; + + @JsonProperty("code") + public String code = DEFAULT_STATUS; + + @Override + public String toString() { + return "{ \"code\": \"" + JSON.escape(code) + "\" }"; + } + } + + @Override + public String toString() { + return "{ \"status\": " + status.toString() + " }"; + } +} diff --git a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java index aaaab22e742..68958c94dfd 100644 --- a/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java +++ b/service-monitor/src/main/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImpl.java @@ -3,8 +3,6 @@ package com.yahoo.vespa.service.monitor.internal.slobrok; import com.google.inject.Inject; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; -import com.yahoo.config.model.api.SuperModelListener; import com.yahoo.config.provision.ApplicationId; import com.yahoo.jrt.slobrok.api.Mirror; import com.yahoo.log.LogLevel; @@ -13,6 +11,7 @@ import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.service.monitor.SlobrokApi; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; import com.yahoo.vespa.service.monitor.internal.MonitorManager; import java.util.HashMap; @@ -21,7 +20,7 @@ import java.util.Optional; import java.util.function.Supplier; import java.util.logging.Logger; -public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi, MonitorManager { +public class SlobrokMonitorManagerImpl implements SlobrokApi, MonitorManager { private static final Logger logger = Logger.getLogger(SlobrokMonitorManagerImpl.class.getName()); @@ -40,7 +39,11 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi } @Override - public void applicationActivated(SuperModel superModel, ApplicationInfo application) { + public void applicationActivated(ApplicationInfo application) { + if (!applicationMonitoredWithSlobrok(application.getApplicationId())) { + return; + } + synchronized (monitor) { SlobrokMonitor slobrokMonitor = slobrokMonitors.computeIfAbsent( application.getApplicationId(), @@ -50,7 +53,11 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi } @Override - public void applicationRemoved(SuperModel superModel, ApplicationId id) { + public void applicationRemoved(ApplicationId id) { + if (!applicationMonitoredWithSlobrok(id)) { + return; + } + synchronized (monitor) { SlobrokMonitor slobrokMonitor = slobrokMonitors.remove(id); if (slobrokMonitor == null) { @@ -79,6 +86,10 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi ClusterId clusterId, ServiceType serviceType, ConfigId configId) { + if (!applicationMonitoredWithSlobrok(applicationId)) { + return ServiceStatus.NOT_CHECKED; + } + Optional<String> slobrokServiceName = findSlobrokServiceName(serviceType, configId); if (slobrokServiceName.isPresent()) { synchronized (monitor) { @@ -95,6 +106,14 @@ public class SlobrokMonitorManagerImpl implements SuperModelListener, SlobrokApi } } + private boolean applicationMonitoredWithSlobrok(ApplicationId applicationId) { + if (applicationId.equals(ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId())) { + return false; + } + + return true; + } + /** * Get the Slobrok service name of the service, or empty if the service * is not registered with Slobrok. diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGeneratorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGeneratorTest.java index 58f99786017..899cc59bb34 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ConfigServerAppGeneratorTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/application/ApplicationInstanceGeneratorTest.java @@ -1,22 +1,27 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.application; +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.provision.Zone; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.internal.ConfigserverUtil; import org.junit.Test; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import static com.yahoo.vespa.service.monitor.application.ConfigServerApplication.CONFIG_SERVER_APPLICATION; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -public class ConfigServerAppGeneratorTest { +public class ApplicationInstanceGeneratorTest { private static final String configServer1 = "cfg1.yahoo.com"; private static final String configServer2 = "cfg2.yahoo.com"; private static final String configServer3 = "cfg3.yahoo.com"; @@ -28,9 +33,17 @@ public class ConfigServerAppGeneratorTest { private final ServiceStatusProvider statusProvider = mock(ServiceStatusProvider.class); @Test - public void toApplicationInstance() throws Exception { + public void toApplicationInstance() { when(statusProvider.getStatus(any(), any(), any(), any())).thenReturn(ServiceStatus.NOT_CHECKED); - ApplicationInstance applicationInstance = new ConfigServerAppGenerator(configServerList) + ConfigserverConfig config = ConfigserverUtil.create( + true, + true, + configServer1, + configServer2, + configServer3); + Zone zone = mock(Zone.class); + ApplicationInfo configServer = CONFIG_SERVER_APPLICATION.makeApplicationInfo(config); + ApplicationInstance applicationInstance = new ApplicationInstanceGenerator(configServer, zone) .makeApplicationInstance(statusProvider); assertEquals( diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ConfigserverUtil.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ConfigserverUtil.java new file mode 100644 index 00000000000..85df02949a6 --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ConfigserverUtil.java @@ -0,0 +1,52 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; + +/** + * @author hakon + */ +public class ConfigserverUtil { + /** Create a ConfigserverConfig with the given settings. */ + public static ConfigserverConfig create( + boolean nodeAdminInContainer, + boolean multitenant, + String configServerHostname1, + String configServerHostname2, + String configServerHostname3) { + return new ConfigserverConfig( + new ConfigserverConfig.Builder() + .nodeAdminInContainer(nodeAdminInContainer) + .multitenant(multitenant) + .zookeeperserver(new ConfigserverConfig.Zookeeperserver.Builder().hostname(configServerHostname1).port(1)) + .zookeeperserver(new ConfigserverConfig.Zookeeperserver.Builder().hostname(configServerHostname2).port(2)) + .zookeeperserver(new ConfigserverConfig.Zookeeperserver.Builder().hostname(configServerHostname3).port(3))); + } + + public static ConfigserverConfig createExampleConfigserverConfig() { + return create(true, true, "cfg1", "cfg2", "cfg3"); + } + + public static ConfigserverConfig createExampleConfigserverConfig(boolean nodeAdminInContainer, + boolean multitenant) { + return create(nodeAdminInContainer, multitenant, "cfg1", "cfg2", "cfg3"); + } + + public static ApplicationInfo makeConfigServerApplicationInfo( + String configServerHostname1, + String configServerHostname2, + String configServerHostname3) { + return ConfigServerApplication.CONFIG_SERVER_APPLICATION.makeApplicationInfo(create( + true, + true, + configServerHostname1, + configServerHostname2, + configServerHostname3)); + } + + public static ApplicationInfo makeExampleConfigServer() { + return makeConfigServerApplicationInfo("cfg1", "cfg2", "cfg3"); + } +} diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/DuperModelTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/DuperModelTest.java new file mode 100644 index 00000000000..c9d19d0ccd9 --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/DuperModelTest.java @@ -0,0 +1,53 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.config.model.api.SuperModel; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.service.monitor.ServiceStatusProvider; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * @author hakon + */ +public class DuperModelTest { + private final ServiceStatusProvider statusProvider = mock(ServiceStatusProvider.class); + + @Test + public void toApplicationInstance() { + when(statusProvider.getStatus(any(), any(), any(), any())).thenReturn(ServiceStatus.NOT_CHECKED); + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + DuperModel duperModel = new DuperModel(config); + SuperModel superModel = mock(SuperModel.class); + ApplicationInfo superModelApplicationInfo = mock(ApplicationInfo.class); + when(superModel.getAllApplicationInfos()).thenReturn(Collections.singletonList(superModelApplicationInfo)); + List<ApplicationInfo> applicationInfos = duperModel.getApplicationInfos(superModel); + assertEquals(2, applicationInfos.size()); + assertEquals(ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId(), applicationInfos.get(0).getApplicationId()); + assertSame(superModelApplicationInfo, applicationInfos.get(1)); + } + + @Test + public void toApplicationInstanceInSingleTenantMode() { + when(statusProvider.getStatus(any(), any(), any(), any())).thenReturn(ServiceStatus.NOT_CHECKED); + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(true, false); + DuperModel duperModel = new DuperModel(config); + SuperModel superModel = mock(SuperModel.class); + ApplicationInfo superModelApplicationInfo = mock(ApplicationInfo.class); + when(superModel.getAllApplicationInfos()).thenReturn(Collections.singletonList(superModelApplicationInfo)); + List<ApplicationInfo> applicationInfos = duperModel.getApplicationInfos(superModel); + assertEquals(1, applicationInfos.size()); + assertSame(superModelApplicationInfo, applicationInfos.get(0)); + } +} diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java index a21691ee4d0..5a57451a298 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/ModelGeneratorTest.java @@ -1,6 +1,7 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; +import com.yahoo.cloud.config.ConfigserverConfig; import com.yahoo.config.model.api.SuperModel; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.RegionName; @@ -15,13 +16,9 @@ import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; import org.junit.Test; -import java.util.Collections; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; @@ -35,13 +32,12 @@ public class ModelGeneratorTest { private final int PORT = 2; @Test - public void toApplicationModelWithConfigServerApplication() throws Exception { - SuperModel superModel = - ExampleModel.createExampleSuperModelWithOneRpcPort(HOSTNAME, PORT); + public void toApplicationModel() throws Exception { + SuperModel superModel = ExampleModel.createExampleSuperModelWithOneRpcPort(HOSTNAME, PORT); - List<String> configServerHosts = Stream.of("cfg1", "cfg2", "cfg3") - .collect(Collectors.toList()); - ModelGenerator modelGenerator = new ModelGenerator(configServerHosts); + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + DuperModel duperModel = new DuperModel(config); + ModelGenerator modelGenerator = new ModelGenerator(); Zone zone = new Zone(Environment.from(ENVIRONMENT), RegionName.from(REGION)); @@ -51,7 +47,7 @@ public class ModelGeneratorTest { ServiceModel serviceModel = modelGenerator.toServiceModel( - superModel, + duperModel.getApplicationInfos(superModel), zone, slobrokMonitorManager); @@ -78,32 +74,6 @@ public class ModelGeneratorTest { } } - @Test - public void toApplicationModel() throws Exception { - SuperModel superModel = - ExampleModel.createExampleSuperModelWithOneRpcPort(HOSTNAME, PORT); - ModelGenerator modelGenerator = new ModelGenerator(Collections.emptyList()); - - Zone zone = new Zone(Environment.from(ENVIRONMENT), RegionName.from(REGION)); - - SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); - when(slobrokMonitorManager.getStatus(any(), any(), any(), any())) - .thenReturn(ServiceStatus.UP); - - ServiceModel serviceModel = - modelGenerator.toServiceModel( - superModel, - zone, - slobrokMonitorManager); - - Map<ApplicationInstanceReference, - ApplicationInstance> applicationInstances = - serviceModel.getAllApplicationInstances(); - - assertEquals(1, applicationInstances.size()); - verifyOtherApplication(applicationInstances.values().iterator().next()); - } - private void verifyOtherApplication(ApplicationInstance applicationInstance) { assertEquals(String.format("%s:%s:%s:%s:%s", ExampleModel.TENANT, diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java index 83bad0ddb2a..eb6d6d583f7 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/SuperModelListenerImplTest.java @@ -14,6 +14,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -22,11 +23,13 @@ public class SuperModelListenerImplTest { public void sanityCheck() { SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); ServiceMonitorMetrics metrics = mock(ServiceMonitorMetrics.class); + DuperModel duperModel = mock(DuperModel.class); ModelGenerator modelGenerator = mock(ModelGenerator.class); Zone zone = mock(Zone.class); SuperModelListenerImpl listener = new SuperModelListenerImpl( slobrokMonitorManager, metrics, + duperModel, modelGenerator, zone); @@ -38,13 +41,15 @@ public class SuperModelListenerImplTest { ApplicationInfo application2 = mock(ApplicationInfo.class); List<ApplicationInfo> applications = Stream.of(application1, application2) .collect(Collectors.toList()); - when(superModel.getAllApplicationInfos()).thenReturn(applications); + when(duperModel.getApplicationInfos(superModel)).thenReturn(applications); listener.start(superModelProvider); - verify(slobrokMonitorManager).applicationActivated(superModel, application1); - verify(slobrokMonitorManager).applicationActivated(superModel, application2); + verify(duperModel, times(1)).getApplicationInfos(superModel); + verify(slobrokMonitorManager).applicationActivated(application1); + verify(slobrokMonitorManager).applicationActivated(application2); ServiceModel serviceModel = listener.get(); - verify(modelGenerator).toServiceModel(superModel, zone, slobrokMonitorManager); + verify(duperModel, times(2)).getApplicationInfos(superModel); + verify(modelGenerator).toServiceModel(applications, zone, slobrokMonitorManager); } }
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java index b7c3ed8e1e1..79916e43712 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/UnionMonitorManagerTest.java @@ -1,95 +1,44 @@ // Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.service.monitor.internal; -import com.yahoo.cloud.config.ConfigserverConfig; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; -import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.applicationmodel.ServiceStatus; import com.yahoo.vespa.service.monitor.internal.health.HealthMonitorManager; import com.yahoo.vespa.service.monitor.internal.slobrok.SlobrokMonitorManagerImpl; import org.junit.Test; import static com.yahoo.vespa.applicationmodel.ClusterId.NODE_ADMIN; +import static com.yahoo.vespa.applicationmodel.ServiceStatus.*; +import static com.yahoo.vespa.applicationmodel.ServiceStatus.NOT_CHECKED; +import static com.yahoo.vespa.applicationmodel.ServiceStatus.UP; import static com.yahoo.vespa.applicationmodel.ServiceType.CONTAINER; import static com.yahoo.vespa.service.monitor.application.ZoneApplication.ZONE_APPLICATION_ID; +import static org.junit.Assert.assertSame; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class UnionMonitorManagerTest { - @Test - public void nodeAdminInContainer() { - testWith( - true, - ZONE_APPLICATION_ID, - NODE_ADMIN, - CONTAINER, - 1, - 0); - } - - @Test - public void nodeAdminOutsideContainer() { - boolean inContainer = false; - - // When nodeAdminInContainer is set, then only the node admin cluster should use health - testWith( - inContainer, - ZONE_APPLICATION_ID, - NODE_ADMIN, - CONTAINER, - 0, - 1); - - testWith( - inContainer, - ApplicationId.fromSerializedForm("a:b:default"), - NODE_ADMIN, - CONTAINER, - 1, - 0); + private final SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); + private final HealthMonitorManager healthMonitorManager = mock(HealthMonitorManager.class); - testWith( - inContainer, - ZONE_APPLICATION_ID, - new ClusterId("foo"), - CONTAINER, - 1, - 0); + private final UnionMonitorManager manager = new UnionMonitorManager( + slobrokMonitorManager, + healthMonitorManager); - testWith( - inContainer, - ZONE_APPLICATION_ID, - NODE_ADMIN, - new ServiceType("foo"), - 1, - 0); + @Test + public void verifyHealthTakesPriority() { + testWith(UP, DOWN, UP); + testWith(NOT_CHECKED, DOWN, DOWN); + testWith(NOT_CHECKED, NOT_CHECKED, NOT_CHECKED); } - private void testWith(boolean nodeAdminInContainer, - ApplicationId applicationId, - ClusterId clusterId, - ServiceType serviceType, - int expectedSlobrokCalls, - int expectedHealthCalls) { - SlobrokMonitorManagerImpl slobrokMonitorManager = mock(SlobrokMonitorManagerImpl.class); - HealthMonitorManager healthMonitorManager = mock(HealthMonitorManager.class); - - ConfigserverConfig.Builder builder = new ConfigserverConfig.Builder(); - builder.nodeAdminInContainer(nodeAdminInContainer); - ConfigserverConfig config = new ConfigserverConfig(builder); - - - UnionMonitorManager manager = new UnionMonitorManager( - slobrokMonitorManager, - healthMonitorManager, - config); - - manager.getStatus(applicationId, clusterId, serviceType, new ConfigId("config-id")); - - verify(slobrokMonitorManager, times(expectedSlobrokCalls)).getStatus(any(), any(), any(), any()); - verify(healthMonitorManager, times(expectedHealthCalls)).getStatus(any(), any(), any(), any()); + private void testWith(ServiceStatus healthStatus, + ServiceStatus slobrokStatus, + ServiceStatus expectedStatus) { + when(healthMonitorManager.getStatus(any(), any(), any(), any())).thenReturn(healthStatus); + when(slobrokMonitorManager.getStatus(any(), any(), any(), any())).thenReturn(slobrokStatus); + ServiceStatus status = manager.getStatus(ZONE_APPLICATION_ID, NODE_ADMIN, CONTAINER, new ConfigId("config-id")); + assertSame(expectedStatus, status); } }
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitorTest.java new file mode 100644 index 00000000000..51b0503565f --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/ApplicationHealthMonitorTest.java @@ -0,0 +1,24 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.service.monitor.application.ConfigServerApplication; +import com.yahoo.vespa.service.monitor.internal.ConfigserverUtil; +import org.junit.Test; + +import static com.yahoo.vespa.applicationmodel.ServiceStatus.NOT_CHECKED; +import static org.junit.Assert.assertEquals; + +public class ApplicationHealthMonitorTest { + @Test + public void sanityCheck() { + ApplicationHealthMonitor monitor = ApplicationHealthMonitor.startMonitoring( + ConfigserverUtil.makeExampleConfigServer()); + ServiceStatus status = monitor.getStatus( + ConfigServerApplication.CONFIG_SERVER_APPLICATION.getApplicationId(), + ConfigServerApplication.CLUSTER_ID, + ConfigServerApplication.SERVICE_TYPE, + ConfigServerApplication.configIdFrom(0)); + assertEquals(NOT_CHECKED, status); + } +}
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManagerTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManagerTest.java new file mode 100644 index 00000000000..b9d25406f9b --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorManagerTest.java @@ -0,0 +1,49 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.cloud.config.ConfigserverConfig; +import com.yahoo.config.model.api.ApplicationInfo; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.service.monitor.application.ZoneApplication; +import com.yahoo.vespa.service.monitor.internal.ConfigserverUtil; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class HealthMonitorManagerTest { + @Test + public void addRemove() { + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + HealthMonitorManager manager = new HealthMonitorManager(config); + ApplicationInfo applicationInfo = ConfigserverUtil.makeExampleConfigServer(); + manager.applicationActivated(applicationInfo); + manager.applicationRemoved(applicationInfo.getApplicationId()); + } + + @Test + public void withNodeAdmin() { + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(); + HealthMonitorManager manager = new HealthMonitorManager(config); + ServiceStatus status = manager.getStatus( + ZoneApplication.ZONE_APPLICATION_ID, + ClusterId.NODE_ADMIN, + ServiceType.CONTAINER, + new ConfigId("config-id-1")); + assertEquals(ServiceStatus.NOT_CHECKED, status); + } + + @Test + public void withHostAdmin() { + ConfigserverConfig config = ConfigserverUtil.createExampleConfigserverConfig(false, true); + HealthMonitorManager manager = new HealthMonitorManager(config); + ServiceStatus status = manager.getStatus( + ZoneApplication.ZONE_APPLICATION_ID, + ClusterId.NODE_ADMIN, + ServiceType.CONTAINER, + new ConfigId("config-id-1")); + assertEquals(ServiceStatus.UP, status); + } +}
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorTest.java new file mode 100644 index 00000000000..cca1530ad97 --- /dev/null +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/health/HealthMonitorTest.java @@ -0,0 +1,21 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.service.monitor.internal.health; + +import com.yahoo.vespa.applicationmodel.ServiceStatus; +import org.junit.Test; + +import java.net.MalformedURLException; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +public class HealthMonitorTest { + @Test + public void basicTests() throws MalformedURLException { + HealthClient healthClient = mock(HealthClient.class); + try (HealthMonitor monitor = new HealthMonitor(healthClient)) { + monitor.startMonitoring(); + assertEquals(ServiceStatus.NOT_CHECKED, monitor.getStatus()); + } + } +}
\ No newline at end of file diff --git a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java index 8e4443df83b..a567559980b 100644 --- a/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java +++ b/service-monitor/src/test/java/com/yahoo/vespa/service/monitor/internal/slobrok/SlobrokMonitorManagerImplTest.java @@ -2,7 +2,7 @@ package com.yahoo.vespa.service.monitor.internal.slobrok; import com.yahoo.config.model.api.ApplicationInfo; -import com.yahoo.config.model.api.SuperModel; +import com.yahoo.config.provision.ApplicationId; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.ServiceStatus; @@ -28,18 +28,19 @@ public class SlobrokMonitorManagerImplTest { private final SlobrokMonitorManagerImpl slobrokMonitorManager = new SlobrokMonitorManagerImpl(slobrokMonitorFactory); private final SlobrokMonitor slobrokMonitor = mock(SlobrokMonitor.class); - private final SuperModel superModel = mock(SuperModel.class); + private final ApplicationId applicationId = ApplicationId.from("tenant", "app", "instance"); private final ApplicationInfo application = mock(ApplicationInfo.class); private final ClusterId clusterId = new ClusterId("cluster-id"); @Before public void setup() { when(slobrokMonitorFactory.get()).thenReturn(slobrokMonitor); + when(application.getApplicationId()).thenReturn(applicationId); } @Test public void testActivationOfApplication() { - slobrokMonitorManager.applicationActivated(superModel, application); + slobrokMonitorManager.applicationActivated(application); verify(slobrokMonitorFactory, times(1)).get(); } @@ -51,14 +52,14 @@ public class SlobrokMonitorManagerImplTest { @Test public void testGetStatus_ApplicationInSlobrok() { - slobrokMonitorManager.applicationActivated(superModel, application); + slobrokMonitorManager.applicationActivated(application); when(slobrokMonitor.registeredInSlobrok("config.id")).thenReturn(true); assertEquals(ServiceStatus.UP, getStatus("topleveldispatch")); } @Test public void testGetStatus_ServiceNotInSlobrok() { - slobrokMonitorManager.applicationActivated(superModel, application); + slobrokMonitorManager.applicationActivated(application); when(slobrokMonitor.registeredInSlobrok("config.id")).thenReturn(false); assertEquals(ServiceStatus.DOWN, getStatus("topleveldispatch")); } diff --git a/valgrind-suppressions.txt b/valgrind-suppressions.txt index 2df6c9c5691..2587552ceff 100644 --- a/valgrind-suppressions.txt +++ b/valgrind-suppressions.txt @@ -339,3 +339,20 @@ fun:__static_initialization_and_destruction_0 ... } +{ + Apparent memory leak on Fedora 28. + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:tsearch + fun:__add_to_environ + fun:setenv +} +{ + Apparent memory leak on Fedora 28. + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:__add_to_environ + fun:setenv +} diff --git a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java index 12389712976..ab127b19bf1 100644 --- a/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java +++ b/vespa-athenz/src/main/java/com/yahoo/vespa/athenz/identityprovider/api/EntityBindingsMapper.java @@ -10,9 +10,12 @@ import com.yahoo.vespa.athenz.identityprovider.api.bindings.SignedIdentityDocume import com.yahoo.vespa.athenz.identityprovider.api.bindings.VespaUniqueInstanceIdEntity; import com.yahoo.vespa.athenz.utils.AthenzIdentities; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.Base64; -import static com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId.*; +import static com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId.fromDottedString; /** * Utility class for mapping objects model types and their Jackson binding versions. @@ -102,4 +105,22 @@ public class EntityBindingsMapper { } } + public static SignedIdentityDocument readSignedIdentityDocumentFromFile(Path file) { + try { + SignedIdentityDocumentEntity entity = mapper.readValue(file.toFile(), SignedIdentityDocumentEntity.class); + return EntityBindingsMapper.toSignedIdentityDocument(entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public static void writeSignedIdentityDocumentToFile(Path file, SignedIdentityDocument document) { + try { + SignedIdentityDocumentEntity entity = EntityBindingsMapper.toSignedIdentityDocumentEntity(document); + mapper.writeValue(file.toFile(), entity); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } |