diff options
26 files changed, 1236 insertions, 71 deletions
diff --git a/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ApplicationInstanceReference.java b/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ApplicationInstanceReference.java index d8feec5af3b..570e0d632ed 100644 --- a/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ApplicationInstanceReference.java +++ b/application-model/src/main/java/com/yahoo/vespa/applicationmodel/ApplicationInstanceReference.java @@ -33,6 +33,10 @@ public class ApplicationInstanceReference { @JsonValue @Override public String toString() { + return asString(); + } + + public String asString() { return tenantId.s() + ":" + applicationInstanceId.s(); } diff --git a/application-model/src/main/java/com/yahoo/vespa/applicationmodel/HostName.java b/application-model/src/main/java/com/yahoo/vespa/applicationmodel/HostName.java index 0b1998059f0..6fea6846ec9 100644 --- a/application-model/src/main/java/com/yahoo/vespa/applicationmodel/HostName.java +++ b/application-model/src/main/java/com/yahoo/vespa/applicationmodel/HostName.java @@ -7,7 +7,7 @@ import java.util.Objects; /** * @author bjorncs */ -public class HostName { +public class HostName implements Comparable<HostName> { private final String id; @@ -40,4 +40,9 @@ public class HostName { public int hashCode() { return Objects.hash(id); } + + @Override + public int compareTo(HostName o) { + return id.compareTo(o.id); + } } diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorImpl.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorImpl.java index 7a9623ecffd..8019ee7725a 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorImpl.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorImpl.java @@ -13,6 +13,7 @@ import com.yahoo.vespa.orchestrator.controller.ClusterControllerClient; import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; import com.yahoo.vespa.orchestrator.controller.ClusterControllerState; import com.yahoo.vespa.orchestrator.controller.ClusterControllerStateResponse; +import com.yahoo.vespa.orchestrator.model.VespaModelUtil; import com.yahoo.vespa.orchestrator.policy.BatchHostStateChangeDeniedException; import com.yahoo.vespa.orchestrator.policy.HostStateChangeDeniedException; import com.yahoo.vespa.orchestrator.policy.HostedVespaPolicy; @@ -294,7 +295,7 @@ public class OrchestratorImpl implements Orchestrator { contentClusterIds,application.applicationInstanceId(),state)); for (ClusterId clusterId : contentClusterIds) { ClusterControllerClient client = clusterControllerClientFactory.createClient( - VespaModelUtil.getClusterControllerInstances(application, clusterId), + VespaModelUtil.getClusterControllerInstancesInOrder(application, clusterId), clusterId.s()); try { ClusterControllerStateResponse response = client.setApplicationState(state); diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorUtil.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorUtil.java index 565a96de13b..aa5b4fd78c0 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorUtil.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/OrchestratorUtil.java @@ -126,7 +126,7 @@ public class OrchestratorUtil { public static ApplicationId toApplicationId(ApplicationInstanceReference appRef) { - String appNameStr = appRef.toString(); + String appNameStr = appRef.asString(); String[] appNameParts = appNameStr.split(":"); // Env, region and instance seems to be optional due to the hardcoded config server app diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactory.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactory.java index e25957d3eaf..56bba91a016 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactory.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactory.java @@ -1,15 +1,15 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.orchestrator.controller; -import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.applicationmodel.HostName; -import java.util.Collection; +import java.util.List; /** * @author bakksjo */ public interface ClusterControllerClientFactory { - ClusterControllerClient createClient(Collection<? extends ServiceInstance<?>> clusterControllers, String clusterName); + ClusterControllerClient createClient(List<HostName> clusterControllers, String clusterName); } diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/RetryingClusterControllerClientFactory.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/RetryingClusterControllerClientFactory.java index 375b0a84eb3..8c70622cfba 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/RetryingClusterControllerClientFactory.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/RetryingClusterControllerClientFactory.java @@ -3,13 +3,12 @@ package com.yahoo.vespa.orchestrator.controller; import com.google.inject.Inject; import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceInstance; import com.yahoo.vespa.jaxrs.client.JaxRsClientFactory; import com.yahoo.vespa.jaxrs.client.JaxRsStrategy; import com.yahoo.vespa.jaxrs.client.JaxRsStrategyFactory; import com.yahoo.vespa.jaxrs.client.JerseyJaxRsClientFactory; -import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -36,13 +35,11 @@ public class RetryingClusterControllerClientFactory implements ClusterController } @Override - public ClusterControllerClient createClient(Collection<? extends ServiceInstance<?>> clusterControllers, + public ClusterControllerClient createClient(List<HostName> clusterControllers, String clusterName) { - Set<HostName> hostNames = clusterControllers.stream() - .map(s -> s.hostName()) - .collect(Collectors.toSet()); + Set<HostName> clusterControllerSet = clusterControllers.stream().collect(Collectors.toSet()); JaxRsStrategy<ClusterControllerJaxRsApi> jaxRsApi - = new JaxRsStrategyFactory(hostNames, HARDCODED_CLUSTERCONTROLLER_PORT, jaxRsClientFactory) + = new JaxRsStrategyFactory(clusterControllerSet, HARDCODED_CLUSTERCONTROLLER_PORT, jaxRsClientFactory) .apiWithRetries(ClusterControllerJaxRsApi.class, CLUSTERCONTROLLER_API_PATH); return new ClusterControllerClientImpl(jaxRsApi, clusterName); } diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactory.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactory.java index 8a245edd187..66925b5e3ce 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactory.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactory.java @@ -2,18 +2,14 @@ package com.yahoo.vespa.orchestrator.controller; import com.yahoo.log.LogLevel; +import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.jaxrs.client.JaxRsClientFactory; import com.yahoo.vespa.jaxrs.client.JaxRsStrategy; import com.yahoo.vespa.jaxrs.client.NoRetryJaxRsStrategy; -import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import java.util.Collection; -import java.util.Comparator; +import java.util.List; import java.util.logging.Logger; -import static com.yahoo.vespa.orchestrator.VespaModelUtil.getClusterControllerIndex; - /** * @author bakksjo */ @@ -24,10 +20,6 @@ public class SingleInstanceClusterControllerClientFactory implements ClusterCont private static final Logger log = Logger.getLogger(SingleInstanceClusterControllerClientFactory.class.getName()); - private static final Comparator<ServiceInstance<?>> CLUSTER_CONTROLLER_INDEX_COMPARATOR = Comparator.comparing( - serviceInstance -> - getClusterControllerIndex(serviceInstance.configId())); - private JaxRsClientFactory jaxRsClientFactory; public SingleInstanceClusterControllerClientFactory(JaxRsClientFactory jaxRsClientFactory) { @@ -35,12 +27,12 @@ public class SingleInstanceClusterControllerClientFactory implements ClusterCont } @Override - public ClusterControllerClient createClient(Collection<? extends ServiceInstance<?>> clusterControllers, + public ClusterControllerClient createClient(List<HostName> clusterControllers, String clusterName) { - ServiceInstance<?> serviceInstance = clusterControllers.stream() - .min(CLUSTER_CONTROLLER_INDEX_COMPARATOR) - .orElseThrow(() -> new IllegalArgumentException("No cluster controller instances found")); - HostName controllerHostName = serviceInstance.hostName(); + if (clusterControllers.isEmpty()) { + throw new IllegalArgumentException("No cluster controller instances found"); + } + HostName controllerHostName = clusterControllers.iterator().next(); int port = CLUSTERCONTROLLER_HARDCODED_PORT; // TODO: Get this from service monitor. log.log(LogLevel.DEBUG, () -> diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ApplicationApi.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ApplicationApi.java new file mode 100644 index 00000000000..0ac602b106e --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ApplicationApi.java @@ -0,0 +1,22 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.orchestrator.status.HostStatus; + +import java.util.List; + +/** + * The API a Policy has access to + */ +public interface ApplicationApi { + String applicationInfo(); + + List<ClusterApi> getClusters(); + + void setHostState(HostName hostName, HostStatus status); + List<HostName> getNodesInGroupWithStatus(HostStatus status); + + List<StorageNode> getUpStorageNodesInGroupInClusterOrder(); + List<StorageNode> getStorageNodesAllowedToBeDownInGroupInReverseClusterOrder(); +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ApplicationApiImpl.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ApplicationApiImpl.java new file mode 100644 index 00000000000..5f84c80a051 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ApplicationApiImpl.java @@ -0,0 +1,139 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; +import com.yahoo.vespa.orchestrator.status.HostStatus; +import com.yahoo.vespa.orchestrator.status.MutableStatusRegistry; +import com.yahoo.vespa.orchestrator.status.ReadOnlyStatusRegistry; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; + +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.yahoo.vespa.orchestrator.OrchestratorUtil.getHostsUsedByApplicationInstance; + +public class ApplicationApiImpl implements ApplicationApi { + private final ApplicationInstance<ServiceMonitorStatus> applicationInstance; + private final NodeGroup nodeGroup; + private final MutableStatusRegistry hostStatusService; + private final List<ClusterApi> clusterInOrder; + private final ClusterControllerClientFactory clusterControllerClientFactory; + private final Map<HostName, HostStatus> hostStatusMap; + + public ApplicationApiImpl(NodeGroup nodeGroup, + MutableStatusRegistry hostStatusService, + ClusterControllerClientFactory clusterControllerClientFactory) { + this.applicationInstance = nodeGroup.getApplication(); + this.nodeGroup = nodeGroup; + this.hostStatusService = hostStatusService; + this.hostStatusMap = createHostStatusMap( + getHostsUsedByApplicationInstance(applicationInstance), + hostStatusService); + this.clusterInOrder = makeClustersInOrder(nodeGroup, hostStatusMap, clusterControllerClientFactory); + this.clusterControllerClientFactory = clusterControllerClientFactory; + } + + @Override + public String applicationInfo() { + return applicationInstance.reference().toString(); + } + + private static Map<HostName, HostStatus> createHostStatusMap(Collection<HostName> hosts, + ReadOnlyStatusRegistry hostStatusService) { + return hosts.stream() + .collect(Collectors.toMap( + hostName -> hostName, + hostName -> hostStatusService.getHostStatus(hostName))); + } + + private HostStatus getHostStatus(HostName hostName) { + return hostStatusMap.getOrDefault(hostName, HostStatus.NO_REMARKS); + } + + @Override + public List<ClusterApi> getClusters() { + return clusterInOrder; + } + + @Override + public List<StorageNode> getStorageNodesAllowedToBeDownInGroupInReverseClusterOrder() { + return clusterInOrder.stream() + .map(ClusterApi::storageNodeInGroup) + .filter(Optional::isPresent) + .map(Optional::get) + .filter(storageNode -> getHostStatus(storageNode.hostName()) == HostStatus.ALLOWED_TO_BE_DOWN) + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + } + + @Override + public List<StorageNode> getUpStorageNodesInGroupInClusterOrder() { + return clusterInOrder.stream() + .map(ClusterApi::upStorageNodeInGroup) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + } + + @Override + public void setHostState(HostName hostName, HostStatus status) { + hostStatusService.setHostState(hostName, status); + } + + @Override + public List<HostName> getNodesInGroupWithStatus(HostStatus status) { + return nodeGroup.getHostNames().stream() + .filter(hostName -> getHostStatus(hostName) == status) + .collect(Collectors.toList()); + } + + private static List<ClusterApi> makeClustersInOrder + (NodeGroup nodeGroup, + Map<HostName, HostStatus> hostStatusMap, + ClusterControllerClientFactory clusterControllerClientFactory) { + Set<ServiceCluster<ServiceMonitorStatus>> clustersInGroup = getServiceClustersInGroup(nodeGroup); + return clustersInGroup.stream() + .map(serviceCluster -> new ClusterApiImpl( + serviceCluster, + nodeGroup, + hostStatusMap, + clusterControllerClientFactory)) + .sorted(ApplicationApiImpl::compareClusters) + .collect(Collectors.toList()); + } + + private static int compareClusters(ClusterApi lhs, ClusterApi rhs) { + int diff = lhs.serviceType().toString().compareTo(rhs.serviceType().toString()); + if (diff != 0) { + return diff; + } + + return lhs.clusterId().toString().compareTo(rhs.clusterId().toString()); + } + + private static Set<ServiceCluster<ServiceMonitorStatus>> getServiceClustersInGroup(NodeGroup nodeGroup) { + ApplicationInstance<ServiceMonitorStatus> applicationInstance = nodeGroup.getApplication(); + + Set<ServiceCluster<ServiceMonitorStatus>> serviceClustersInGroup = new HashSet<>(); + for (ServiceCluster<ServiceMonitorStatus> cluster : applicationInstance.serviceClusters()) { + for (ServiceInstance<ServiceMonitorStatus> instance : cluster.serviceInstances()) { + if (nodeGroup.contains(instance.hostName())) { + serviceClustersInGroup.add(cluster); + break; + } + } + } + + return serviceClustersInGroup; + } +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java new file mode 100644 index 00000000000..daa4cea6570 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApi.java @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ServiceType; + +import java.util.Optional; + +public interface ClusterApi { + NodeGroup getNodeGroup(); + + String clusterInfo(); + ClusterId clusterId(); + ServiceType serviceType(); + boolean isStorageCluster(); + + boolean noServicesInGroupIsUp(); + boolean noServicesOutsideGroupIsDown(); + + int percentageOfServicesDown(); + int percentageOfServicesDownIfGroupIsAllowedToBeDown(); + + Optional<StorageNode> storageNodeInGroup(); + Optional<StorageNode> upStorageNodeInGroup(); + + String servicesDownAndNotInGroupDescription(); + String nodesAllowedToBeDownNotInGroupDescription(); +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java new file mode 100644 index 00000000000..5ac34d8df4d --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/ClusterApiImpl.java @@ -0,0 +1,186 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; +import com.yahoo.vespa.orchestrator.status.HostStatus; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +class ClusterApiImpl implements ClusterApi { + private final ServiceCluster<ServiceMonitorStatus> serviceCluster; + private final NodeGroup nodeGroup; + private final Map<HostName, HostStatus> hostStatusMap; + private final ClusterControllerClientFactory clusterControllerClientFactory; + private final Set<ServiceInstance<ServiceMonitorStatus>> servicesInGroup; + private final Set<ServiceInstance<ServiceMonitorStatus>> servicesDownInGroup; + private final Set<ServiceInstance<ServiceMonitorStatus>> servicesNotInGroup; + private final Set<ServiceInstance<ServiceMonitorStatus>> servicesDownAndNotInGroup; + + public ClusterApiImpl(ServiceCluster<ServiceMonitorStatus> serviceCluster, + NodeGroup nodeGroup, + Map<HostName, HostStatus> hostStatusMap, + ClusterControllerClientFactory clusterControllerClientFactory) { + this.serviceCluster = serviceCluster; + this.nodeGroup = nodeGroup; + this.hostStatusMap = hostStatusMap; + this.clusterControllerClientFactory = clusterControllerClientFactory; + + Map<Boolean, Set<ServiceInstance<ServiceMonitorStatus>>> serviceInstancesByLocality = + serviceCluster.serviceInstances().stream() + .collect( + Collectors.groupingBy( + instance -> nodeGroup.contains(instance.hostName()), + Collectors.toSet())); + servicesInGroup = serviceInstancesByLocality.getOrDefault(true, Collections.emptySet()); + servicesNotInGroup = serviceInstancesByLocality.getOrDefault(false, Collections.emptySet()); + + servicesDownInGroup = servicesInGroup.stream().filter(this::serviceEffectivelyDown).collect(Collectors.toSet()); + servicesDownAndNotInGroup = servicesNotInGroup.stream().filter(this::serviceEffectivelyDown).collect(Collectors.toSet()); + } + + @Override + public NodeGroup getNodeGroup() { + return nodeGroup; + } + + @Override + public ClusterId clusterId() { + return serviceCluster.clusterId(); + } + + @Override + public ServiceType serviceType() { + return serviceCluster.serviceType(); + } + + @Override + public boolean isStorageCluster() { + return VespaModelUtil.isStorage(serviceCluster); + } + + @Override + public boolean noServicesInGroupIsUp() { + return servicesDownInGroup.size() == servicesInGroup.size(); + } + + @Override + public boolean noServicesOutsideGroupIsDown() { + return servicesDownAndNotInGroup.size() == 0; + } + + @Override + public int percentageOfServicesDown() { + int numberOfServicesDown = servicesDownAndNotInGroup.size() + servicesDownInGroup.size(); + return numberOfServicesDown * 100 / serviceCluster.serviceInstances().size(); + } + + @Override + public int percentageOfServicesDownIfGroupIsAllowedToBeDown() { + int numberOfServicesDown = servicesDownAndNotInGroup.size() + servicesInGroup.size(); + return numberOfServicesDown * 100 / serviceCluster.serviceInstances().size(); + } + + @Override + public String servicesDownAndNotInGroupDescription() { + // Sort these for readability and testing stability + return servicesDownAndNotInGroup.stream() + .map(service -> service.toString()) + .sorted() + .collect(Collectors.toList()) + .toString(); + } + + @Override + public String nodesAllowedToBeDownNotInGroupDescription() { + return servicesNotInGroup.stream() + .map(ServiceInstance::hostName) + .filter(hostName -> hostStatus(hostName) == HostStatus.ALLOWED_TO_BE_DOWN) + .sorted() + .distinct() + .collect(Collectors.toList()) + .toString(); + } + + private Optional<StorageNode> storageNodeInGroup( + Predicate<ServiceInstance<ServiceMonitorStatus>> storageServicePredicate) { + if (!VespaModelUtil.isStorage(serviceCluster)) { + return Optional.empty(); + } + + Set<StorageNode> storageNodes = new HashSet<>(); + + for (ServiceInstance<ServiceMonitorStatus> serviceInstance : servicesInGroup) { + if (!storageServicePredicate.test(serviceInstance)) { + continue; + } + + HostName hostName = serviceInstance.hostName(); + if (nodeGroup.contains(hostName)) { + if (storageNodes.contains(hostName)) { + throw new IllegalStateException("Found more than 1 storage service instance on " + hostName + + ": last service instance is " + serviceInstance.configId() + + " in storage cluster " + clusterInfo()); + } + + StorageNode storageNode = new StorageNodeImpl( + nodeGroup.getApplication(), + clusterId(), + serviceInstance, + clusterControllerClientFactory); + storageNodes.add(storageNode); + } + } + + if (storageNodes.size() > 1) { + throw new IllegalStateException("Found more than 1 storage node (" + storageNodes + + ") in the same cluster (" + clusterInfo() + ") in the same node group (" + + getNodeGroup().toCommaSeparatedString() + "): E.g. suspension of such a setup is not supported " + + " by the Cluster Controller and is dangerous w.r.t. data redundancy."); + } + + return storageNodes.stream().findFirst(); + } + + @Override + public Optional<StorageNode> storageNodeInGroup() { + return storageNodeInGroup(serviceInstance-> true); + } + + @Override + public Optional<StorageNode> upStorageNodeInGroup() { + return storageNodeInGroup(serviceInstance-> !serviceEffectivelyDown(serviceInstance)); + } + + @Override + public String clusterInfo() { + return "{ clusterId=" + clusterId() + ", serviceType=" + serviceType() + " }"; + } + + private HostStatus hostStatus(HostName hostName) { + return hostStatusMap.getOrDefault(hostName, HostStatus.NO_REMARKS); + } + + private boolean serviceEffectivelyDown(ServiceInstance<ServiceMonitorStatus> service) { + if (hostStatus(service.hostName()) == HostStatus.ALLOWED_TO_BE_DOWN) { + return true; + } + + if (service.serviceStatus() == ServiceMonitorStatus.DOWN) { + return true; + } + + return false; + } +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/NodeGroup.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/NodeGroup.java new file mode 100644 index 00000000000..ff38f39d6ec --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/NodeGroup.java @@ -0,0 +1,75 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.ApplicationInstanceReference; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A group of nodes belonging to the same application instance. + */ +public class NodeGroup { + private final ApplicationInstance<ServiceMonitorStatus> application; + private final Set<HostName> hostNames = new HashSet<>(); + + public NodeGroup(ApplicationInstance<ServiceMonitorStatus> application, HostName... hostNames) { + this.application = application; + this.hostNames.addAll(Arrays.asList(hostNames)); + } + + public void addNode(HostName hostName) { + if (!this.hostNames.add(hostName)) { + throw new IllegalArgumentException("Node " + hostName + " is already in the group"); + } + } + + public ApplicationInstanceReference getApplicationReference() { + return application.reference(); + } + + ApplicationInstance<ServiceMonitorStatus> getApplication() { + return application; + } + + public boolean contains(HostName hostName) { + return hostNames.contains(hostName); + } + + public List<HostName> getHostNames() { + return hostNames.stream().collect(Collectors.toList()).stream().sorted().collect(Collectors.toList()); + } + + public String toCommaSeparatedString() { + return getHostNames().stream().map(HostName::toString).collect(Collectors.joining(",")); + } + + @Override + public String toString() { + return "NodeGroup{" + + "application=" + application.reference() + + ", hostNames=" + hostNames + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof NodeGroup)) return false; + NodeGroup nodeGroup = (NodeGroup) o; + return Objects.equals(application, nodeGroup.application) && + Objects.equals(hostNames, nodeGroup.hostNames); + } + + @Override + public int hashCode() { + return Objects.hash(application, hostNames); + } +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/StorageNode.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/StorageNode.java new file mode 100644 index 00000000000..1698770904f --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/StorageNode.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerState; +import com.yahoo.vespa.orchestrator.policy.HostStateChangeDeniedException; + +public interface StorageNode extends Comparable<StorageNode> { + HostName hostName(); + void setNodeState(ClusterControllerState wantedState) throws HostStateChangeDeniedException; +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/StorageNodeImpl.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/StorageNodeImpl.java new file mode 100644 index 00000000000..1984c5cca40 --- /dev/null +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/StorageNodeImpl.java @@ -0,0 +1,122 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.ClusterId; +import com.yahoo.vespa.applicationmodel.ConfigId; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceInstance; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClient; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerState; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerStateResponse; +import com.yahoo.vespa.orchestrator.policy.HostStateChangeDeniedException; +import com.yahoo.vespa.orchestrator.policy.HostedVespaPolicy; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +public class StorageNodeImpl implements StorageNode { + private static final Logger logger = Logger.getLogger(StorageNodeImpl.class.getName()); + + private final ApplicationInstance<ServiceMonitorStatus> applicationInstance; + private final ClusterId clusterId; + private final ServiceInstance<ServiceMonitorStatus> storageService; + private final ClusterControllerClientFactory clusterControllerClientFactory; + + StorageNodeImpl(ApplicationInstance<ServiceMonitorStatus> applicationInstance, + ClusterId clusterId, + ServiceInstance<ServiceMonitorStatus> storageService, + ClusterControllerClientFactory clusterControllerClientFactory) { + this.applicationInstance = applicationInstance; + this.clusterId = clusterId; + this.storageService = storageService; + this.clusterControllerClientFactory = clusterControllerClientFactory; + } + + @Override + public HostName hostName() { + return storageService.hostName(); + } + + @Override + public void setNodeState(ClusterControllerState wantedNodeState) + throws HostStateChangeDeniedException { + // The "cluster name" used by the Cluster Controller IS the cluster ID. + String clusterId = this.clusterId.s(); + + List<HostName> clusterControllers = VespaModelUtil.getClusterControllerInstancesInOrder(applicationInstance, this.clusterId); + + ClusterControllerClient client = clusterControllerClientFactory.createClient( + clusterControllers, + clusterId); + + ConfigId configId = storageService.configId(); + int nodeIndex = VespaModelUtil.getStorageNodeIndex(configId); + + logger.log(LogLevel.DEBUG, () -> "Setting cluster controller state for " + + "application " + applicationInstance.reference().asString() + + ", host " + hostName() + + ", cluster name " + clusterId + + ", node index " + nodeIndex + + ", node state " + wantedNodeState); + + ClusterControllerStateResponse response; + try { + response = client.setNodeState(nodeIndex, wantedNodeState); + } catch (IOException e) { + throw new HostStateChangeDeniedException( + hostName(), + HostedVespaPolicy.CLUSTER_CONTROLLER_AVAILABLE_CONSTRAINT, + VespaModelUtil.CLUSTER_CONTROLLER_SERVICE_TYPE, + "Failed to communicate with cluster controllers " + clusterControllers + ": " + e, + e); + } + + if ( ! response.wasModified) { + throw new HostStateChangeDeniedException( + hostName(), + HostedVespaPolicy.SET_NODE_STATE_CONSTRAINT, + VespaModelUtil.CLUSTER_CONTROLLER_SERVICE_TYPE, + "Failed to set state to " + wantedNodeState + " in controller: " + response.reason); + } + } + + @Override + public String toString() { + return "StorageNodeImpl{" + + "applicationInstance=" + applicationInstance + + ", clusterId=" + clusterId + + ", storageService=" + storageService + + '}'; + } + + /** Only base it on the service instance, e.g. the cluster ID is included in its equals(). */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StorageNodeImpl)) return false; + StorageNodeImpl that = (StorageNodeImpl) o; + return Objects.equals(storageService, that.storageService); + } + + @Override + public int hashCode() { + return Objects.hash(storageService); + } + + @Override + public int compareTo(StorageNode otherStorageNode) { + if (!(otherStorageNode instanceof StorageNodeImpl)) { + throw new IllegalArgumentException("Unable to compare our class to any StorageNode object"); + } + StorageNodeImpl that = (StorageNodeImpl) otherStorageNode; + + // We're guaranteed there's only one storage service per node. + return this.storageService.hostName().compareTo(that.storageService.hostName()); + } +} diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/VespaModelUtil.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/VespaModelUtil.java index fd210a901d5..a362d7710a9 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/VespaModelUtil.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/model/VespaModelUtil.java @@ -1,5 +1,5 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator; +package com.yahoo.vespa.orchestrator.model; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ClusterId; @@ -11,6 +11,7 @@ import com.yahoo.vespa.applicationmodel.ServiceType; import java.util.Collection; import java.util.Comparator; +import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -39,6 +40,9 @@ public class VespaModelUtil { public static final ServiceType SEARCHNODE_SERVICE_TYPE = new ServiceType("searchnode"); public static final ServiceType STORAGENODE_SERVICE_TYPE = new ServiceType("storagenode"); + private static final Comparator<ServiceInstance<?>> CLUSTER_CONTROLLER_INDEX_COMPARATOR = + Comparator.comparing(serviceInstance -> VespaModelUtil.getClusterControllerIndex(serviceInstance.configId())); + // @return true iff the service cluster refers to a cluster controller service cluster. public static boolean isClusterController(ServiceCluster<?> cluster) { return CLUSTER_CONTROLLER_SERVICE_TYPE.equals(cluster.serviceType()); @@ -66,26 +70,32 @@ public class VespaModelUtil { /** * @return The set of all Cluster Controller service instances for the application. */ - public static <T> Set<ServiceInstance<T>> getClusterControllerInstances(ApplicationInstance<T> application, - ClusterId contentClusterId) + public static <T> List<HostName> getClusterControllerInstancesInOrder(ApplicationInstance<T> application, + ClusterId contentClusterId) { Set<ServiceCluster<T>> controllerClusters = getClusterControllerServiceClusters(application); Collection<ServiceCluster<T>> controllerClustersForContentCluster = filter(controllerClusters, contentClusterId); + Set<ServiceInstance<T>> clusterControllerInstances; if (controllerClustersForContentCluster.size() == 1) { - return first(controllerClustersForContentCluster).serviceInstances(); + clusterControllerInstances = first(controllerClustersForContentCluster).serviceInstances(); } else if (controllerClusters.size() == 1) { ServiceCluster<T> cluster = first(controllerClusters); log.warning("No cluster controller cluster for content cluster " + contentClusterId + ", using the only cluster controller cluster available: " + cluster.clusterId()); - return cluster.serviceInstances(); + clusterControllerInstances = cluster.serviceInstances(); } else { throw new RuntimeException("Failed getting cluster controller for content cluster " + contentClusterId + ". Available clusters = " + controllerClusters + ", matching clusters = " + controllerClustersForContentCluster); } + + return clusterControllerInstances.stream() + .sorted(CLUSTER_CONTROLLER_INDEX_COMPARATOR) + .map(serviceInstance -> serviceInstance.hostName()) + .collect(Collectors.toList()); } private static <T> Collection<ServiceCluster<T>> filter(Set<ServiceCluster<T>> controllerClusters, @@ -110,12 +120,10 @@ public class VespaModelUtil { */ public static HostName getControllerHostName(ApplicationInstance<?> application, ClusterId contentClusterId) { // It happens that the master Cluster Controller is the one with the lowest index, if up. - ServiceInstance<?> serviceInstance = getClusterControllerInstances(application, contentClusterId) - .stream() - .min(Comparator.comparing(instance -> getClusterControllerIndex(instance.configId()))) + return getClusterControllerInstancesInOrder(application, contentClusterId).stream() + .findFirst() .orElseThrow(() -> new IllegalArgumentException("No cluster controllers found in application " + application)); - return serviceInstance.hostName(); } /** diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicy.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicy.java index a85de34097f..b7574a43ce1 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicy.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicy.java @@ -7,11 +7,11 @@ import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.HostName; import com.yahoo.vespa.applicationmodel.ServiceCluster; import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.orchestrator.VespaModelUtil; import com.yahoo.vespa.orchestrator.controller.ClusterControllerClient; import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; import com.yahoo.vespa.orchestrator.controller.ClusterControllerState; import com.yahoo.vespa.orchestrator.controller.ClusterControllerStateResponse; +import com.yahoo.vespa.orchestrator.model.VespaModelUtil; import com.yahoo.vespa.orchestrator.status.HostStatus; import com.yahoo.vespa.orchestrator.status.MutableStatusRegistry; import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; @@ -19,6 +19,7 @@ import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; import java.io.IOException; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; @@ -193,7 +194,7 @@ public class HostedVespaPolicy implements Policy { HostName hostName, ClusterControllerState nodeState) throws HostStateChangeDeniedException { ClusterId contentClusterId = VespaModelUtil.getContentClusterName(application, hostName); - Set<? extends ServiceInstance<?>> clusterControllers = VespaModelUtil.getClusterControllerInstances(application, contentClusterId); + List<HostName> clusterControllers = VespaModelUtil.getClusterControllerInstancesInOrder(application, contentClusterId); ClusterControllerClient client = clusterControllerClientFactory.createClient( clusterControllers, contentClusterId.s()); diff --git a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/ServiceClusterSuspendPolicy.java b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/ServiceClusterSuspendPolicy.java index ba14b616d96..438cbebab44 100644 --- a/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/ServiceClusterSuspendPolicy.java +++ b/orchestrator/src/main/java/com/yahoo/vespa/orchestrator/policy/ServiceClusterSuspendPolicy.java @@ -1,8 +1,8 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.orchestrator.policy; -import com.yahoo.vespa.orchestrator.VespaModelUtil; import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.orchestrator.model.VespaModelUtil; /** * @author hakonhall diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/DummyInstanceLookupService.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/DummyInstanceLookupService.java index d48bc1f302b..dede55f402a 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/DummyInstanceLookupService.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/DummyInstanceLookupService.java @@ -12,6 +12,7 @@ import com.yahoo.vespa.applicationmodel.ServiceCluster; import com.yahoo.vespa.applicationmodel.ServiceInstance; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.orchestrator.model.VespaModelUtil; import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; import java.util.HashSet; diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactoryMock.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactoryMock.java index 4fc9ad2d56d..93dcb5815c6 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactoryMock.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/ClusterControllerClientFactoryMock.java @@ -4,15 +4,14 @@ package com.yahoo.vespa.orchestrator.controller; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ClusterId; import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceInstance; import com.yahoo.vespa.orchestrator.DummyInstanceLookupService; -import com.yahoo.vespa.orchestrator.VespaModelUtil; +import com.yahoo.vespa.orchestrator.model.VespaModelUtil; import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; import java.io.IOException; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -50,7 +49,7 @@ public class ClusterControllerClientFactoryMock implements ClusterControllerClie } @Override - public ClusterControllerClient createClient(Collection<? extends ServiceInstance<?>> clusterControllers, String clusterName) { + public ClusterControllerClient createClient(List<HostName> clusterControllers, String clusterName) { return new ClusterControllerClient() { @Override diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactoryTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactoryTest.java index cc217885047..cde644e7e8b 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactoryTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/controller/SingleInstanceClusterControllerClientFactoryTest.java @@ -1,17 +1,14 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.orchestrator.controller; -import com.yahoo.vespa.jaxrs.client.JaxRsClientFactory; -import com.yahoo.vespa.orchestrator.TestUtil; import com.yahoo.vespa.applicationmodel.ConfigId; import com.yahoo.vespa.applicationmodel.HostName; -import com.yahoo.vespa.applicationmodel.ServiceInstance; -import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; +import com.yahoo.vespa.jaxrs.client.JaxRsClientFactory; import org.junit.Before; import org.junit.Test; -import java.util.Collection; -import java.util.Collections; +import java.util.Arrays; +import java.util.List; import static org.hamcrest.CoreMatchers.anyOf; import static org.hamcrest.CoreMatchers.equalTo; @@ -52,7 +49,7 @@ public class SingleInstanceClusterControllerClientFactoryTest { @Test public void testCreateClientWithNoClusterControllerInstances() throws Exception { - final Collection<ServiceInstance<ServiceMonitorStatus>> clusterControllers = Collections.emptySet(); + final List<HostName> clusterControllers = Arrays.asList(); try { clientFactory.createClient(clusterControllers, "clusterName"); @@ -64,8 +61,7 @@ public class SingleInstanceClusterControllerClientFactoryTest { @Test public void testCreateClientWithSingleClusterControllerInstance() throws Exception { - final Collection<ServiceInstance<ServiceMonitorStatus>> clusterControllers = Collections.singleton( - new ServiceInstance<>(clusterControllerConfigId(1), HOST_NAME_1, ServiceMonitorStatus.UP)); + final List<HostName> clusterControllers = Arrays.asList(HOST_NAME_1); clientFactory.createClient(clusterControllers, "clusterName") .setNodeState(0, ClusterControllerState.MAINTENANCE); @@ -78,10 +74,8 @@ public class SingleInstanceClusterControllerClientFactoryTest { } @Test - public void testCreateClientWithTwoNonClusterControllerInstances() throws Exception { - final Collection<ServiceInstance<ServiceMonitorStatus>> clusterControllers = TestUtil.makeServiceInstanceSet( - new ServiceInstance<>(new ConfigId("not-a-cluster-controller-1"), HOST_NAME_1, ServiceMonitorStatus.UP), - new ServiceInstance<>(new ConfigId("not-a-cluster-controller-2"), HOST_NAME_2, ServiceMonitorStatus.UP)); + public void testCreateClientWithoutClusterControllerInstances() throws Exception { + final List<HostName> clusterControllers = Arrays.asList(); try { clientFactory.createClient(clusterControllers, "clusterName"); @@ -93,10 +87,7 @@ public class SingleInstanceClusterControllerClientFactoryTest { @Test public void testCreateClientWithThreeClusterControllerInstances() throws Exception { - final Collection<ServiceInstance<ServiceMonitorStatus>> clusterControllers = TestUtil.makeServiceInstanceSet( - new ServiceInstance<>(clusterControllerConfigId(1), HOST_NAME_1, ServiceMonitorStatus.UP), - new ServiceInstance<>(clusterControllerConfigId(2), HOST_NAME_2, ServiceMonitorStatus.UP), - new ServiceInstance<>(clusterControllerConfigId(3), HOST_NAME_3, ServiceMonitorStatus.UP)); + final List<HostName> clusterControllers = Arrays.asList(HOST_NAME_1, HOST_NAME_2, HOST_NAME_3); clientFactory.createClient(clusterControllers, "clusterName") .setNodeState(0, ClusterControllerState.MAINTENANCE); diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ApplicationApiImplTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ApplicationApiImplTest.java new file mode 100644 index 00000000000..684f7e76d94 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ApplicationApiImplTest.java @@ -0,0 +1,321 @@ +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.orchestrator.status.HostStatus; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +public class ApplicationApiImplTest { + final ModelTestUtils modelUtils = new ModelTestUtils(); + + @Test + public void testApplicationInfo() { + ApplicationApiImpl applicationApi = + modelUtils.createApplicationApiImpl(modelUtils.createApplicationInstance(new ArrayList<>())); + assertEquals("tenant:application-name:foo:bar:default", applicationApi.applicationInfo()); + } + + @Test + public void testGetClustersThatAreOnAtLeastOneNodeInGroup() { + HostName hostName1 = new HostName("host1"); + HostName hostName2 = new HostName("host2"); + HostName hostName3 = new HostName("host3"); + HostName hostName4 = new HostName("host4"); + + ApplicationInstance<ServiceMonitorStatus> applicationInstance = + modelUtils.createApplicationInstance(Arrays.asList( + modelUtils.createServiceCluster( + "cluster-3", + new ServiceType("service-type-3"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-1", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-2", hostName2, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-1", + new ServiceType("service-type-1"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-3", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-4", hostName3, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-2", + new ServiceType("service-type-2"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-5", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-6", hostName2, ServiceMonitorStatus.UP) + ) + ) + )); + + verifyClustersInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName1), 1, 2, 3); + verifyClustersInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName2), 2, 3); + verifyClustersInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName3), 1); + verifyClustersInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName4)); + } + + private void verifyClustersInOrder(ApplicationApi applicationApi, + Integer... expectedClusterNumbers) { + // Note: we require the clusters to be in order. + List<ClusterApi> clusterApis = applicationApi.getClusters(); + String clusterInfos = clusterApis.stream().map(clusterApi -> clusterApi.clusterInfo()).collect(Collectors.joining(",")); + + String expectedClusterInfos = Arrays.stream(expectedClusterNumbers) + .map(number -> "{ clusterId=cluster-" + number + ", serviceType=service-type-" + number + " }") + .collect(Collectors.joining(",")); + + assertEquals(expectedClusterInfos, clusterInfos); + } + + @Test + public void testGetUpStorageNodesInGroupInClusterOrder() { + HostName hostName1 = new HostName("host1"); + HostName hostName2 = new HostName("host2"); + HostName hostName3 = new HostName("host3"); + HostName hostName4 = new HostName("host4"); + HostName hostName5 = new HostName("host5"); + HostName hostName6 = new HostName("host6"); + HostName hostName7 = new HostName("host7"); + + ApplicationInstance<ServiceMonitorStatus> applicationInstance = + modelUtils.createApplicationInstance(Arrays.asList( + modelUtils.createServiceCluster( + "cluster-3", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("config-id-30", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-31", hostName2, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-1", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("config-id-10", hostName3, ServiceMonitorStatus.DOWN), + modelUtils.createServiceInstance("config-id-11", hostName4, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-4", + new ServiceType("service-type-4"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-40", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-41", hostName2, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-42", hostName3, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-43", hostName5, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-2", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("config-id-20", hostName6, ServiceMonitorStatus.DOWN), + modelUtils.createServiceInstance("config-id-21", hostName7, ServiceMonitorStatus.UP) + ) + ) + )); + + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName1), hostName1); + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName2), hostName2); + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName3)); // host3 is DOWN + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName4), hostName4); + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName5)); // not a storage cluster + + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName1, hostName3), hostName1); + + // For the node group (host1, host4), they both have an up storage node (service instance) + // with clusters (cluster-3, cluster-1) respectively, and so the order of the hosts are reversed + // (host4, host1) when sorted by the clusters. + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl(applicationInstance, hostName1, hostName4), hostName4, hostName1); + + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl( + applicationInstance, hostName1, hostName4, hostName5), hostName4, hostName1); + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl( + applicationInstance, hostName1, hostName4, hostName5, hostName6), hostName4, hostName1); + verifyUpStorageNodesInOrder(modelUtils.createApplicationApiImpl( + applicationInstance, hostName1, hostName4, hostName5, hostName7), hostName4, hostName7, hostName1); + } + + private void verifyUpStorageNodesInOrder(ApplicationApi applicationApi, + HostName... expectedHostNames) { + List<HostName> upStorageNodes = applicationApi.getUpStorageNodesInGroupInClusterOrder().stream() + .map(storageNode -> storageNode.hostName()) + .collect(Collectors.toList()); + assertEquals(Arrays.asList(expectedHostNames), upStorageNodes); + } + + @Test + public void testUpConditionOfStorageNode() { + verifyUpConditionWith(HostStatus.NO_REMARKS, ServiceMonitorStatus.UP, true); + verifyUpConditionWith(HostStatus.NO_REMARKS, ServiceMonitorStatus.NOT_CHECKED, true); + verifyUpConditionWith(HostStatus.NO_REMARKS, ServiceMonitorStatus.DOWN, false); + verifyUpConditionWith(HostStatus.ALLOWED_TO_BE_DOWN, ServiceMonitorStatus.UP, false); + verifyUpConditionWith(HostStatus.ALLOWED_TO_BE_DOWN, ServiceMonitorStatus.NOT_CHECKED, false); + verifyUpConditionWith(HostStatus.ALLOWED_TO_BE_DOWN, ServiceMonitorStatus.DOWN, false); + } + + private void verifyUpConditionWith(HostStatus hostStatus, ServiceMonitorStatus serviceStatus, boolean expectUp) { + HostName hostName1 = modelUtils.createNode("host1", hostStatus); + + ApplicationInstance<ServiceMonitorStatus> applicationInstance = + modelUtils.createApplicationInstance(Arrays.asList( + modelUtils.createServiceCluster( + "cluster-1", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList(modelUtils.createServiceInstance("config-id-1", hostName1, serviceStatus)) + ) + )); + + ApplicationApiImpl applicationApi = modelUtils.createApplicationApiImpl(applicationInstance, hostName1); + List<HostName> upStorageNodes = expectUp ? Arrays.asList(hostName1) : new ArrayList<>(); + + List<HostName> actualStorageNodes = applicationApi.getUpStorageNodesInGroupInClusterOrder().stream() + .map(storageNode -> storageNode.hostName()) + .collect(Collectors.toList()); + assertEquals(upStorageNodes, actualStorageNodes); + } + + @Test + public void testGetNodesInGroupWithStatus() { + HostName hostName1 = modelUtils.createNode("host1", HostStatus.NO_REMARKS); + HostName hostName2 = modelUtils.createNode("host2", HostStatus.NO_REMARKS); + HostName hostName3 = modelUtils.createNode("host3", HostStatus.ALLOWED_TO_BE_DOWN); + + ApplicationInstance<ServiceMonitorStatus> applicationInstance = + modelUtils.createApplicationInstance(Arrays.asList( + modelUtils.createServiceCluster( + "cluster-1", + new ServiceType("service-type-1"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-10", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-11", hostName2, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-2", + new ServiceType("service-type-2"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-20", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-21", hostName3, ServiceMonitorStatus.UP) + ) + ) + )); + + verifyNodesInGroupWithoutRemarks( + modelUtils.createApplicationApiImpl(applicationInstance, hostName1), + Arrays.asList(hostName1), + Arrays.asList()); + verifyNodesInGroupWithoutRemarks( + modelUtils.createApplicationApiImpl(applicationInstance, hostName1, hostName2), + Arrays.asList(hostName1, hostName2), + Arrays.asList()); + verifyNodesInGroupWithoutRemarks( + modelUtils.createApplicationApiImpl(applicationInstance, hostName1, hostName2, hostName3), + Arrays.asList(hostName1, hostName2), + Arrays.asList(hostName3)); + verifyNodesInGroupWithoutRemarks( + modelUtils.createApplicationApiImpl(applicationInstance, hostName3), + Arrays.asList(), + Arrays.asList(hostName3)); + } + + private void verifyNodesInGroupWithoutRemarks(ApplicationApi applicationApi, + List<HostName> noRemarksHostNames, + List<HostName> allowedToBeDownHostNames) { + List<HostName> actualNoRemarksHosts = applicationApi.getNodesInGroupWithStatus(HostStatus.NO_REMARKS); + assertEquals(noRemarksHostNames, actualNoRemarksHosts); + List<HostName> actualAllowedToBeDownHosts = applicationApi.getNodesInGroupWithStatus(HostStatus.ALLOWED_TO_BE_DOWN); + assertEquals(allowedToBeDownHostNames, actualAllowedToBeDownHosts); + } + + @Test + public void testGetStorageNodesAllowedToBeDownInGroupInReverseClusterOrder() { + HostName allowedToBeDownHost1 = modelUtils.createNode("host1", HostStatus.ALLOWED_TO_BE_DOWN); + HostName noRemarksHost2 = modelUtils.createNode("host2", HostStatus.NO_REMARKS); + HostName allowedToBeDownHost3 = modelUtils.createNode("host3", HostStatus.ALLOWED_TO_BE_DOWN); + HostName allowedToBeDownHost4 = modelUtils.createNode("host4", HostStatus.ALLOWED_TO_BE_DOWN); + HostName noRemarksHost5 = modelUtils.createNode("host5", HostStatus.ALLOWED_TO_BE_DOWN); + HostName noRemarksHost6 = modelUtils.createNode("host6", HostStatus.NO_REMARKS); + HostName allowedToBeDownHost7 = modelUtils.createNode("host7", HostStatus.ALLOWED_TO_BE_DOWN); + + ApplicationInstance<ServiceMonitorStatus> applicationInstance = + modelUtils.createApplicationInstance(Arrays.asList( + modelUtils.createServiceCluster( + "cluster-4", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("config-id-40", allowedToBeDownHost1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-41", noRemarksHost2, ServiceMonitorStatus.DOWN) + ) + ), + modelUtils.createServiceCluster( + "cluster-1", + new ServiceType("service-type-1"), + Arrays.asList( + modelUtils.createServiceInstance("config-id-10", allowedToBeDownHost1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-11", allowedToBeDownHost3, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-3", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("config-id-30", allowedToBeDownHost4, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-31", noRemarksHost5, ServiceMonitorStatus.UP) + ) + ), + modelUtils.createServiceCluster( + "cluster-2", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("config-id-20", noRemarksHost6, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("config-id-21", allowedToBeDownHost7, ServiceMonitorStatus.UP) + ) + ) + )); + + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, allowedToBeDownHost1), allowedToBeDownHost1); + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, noRemarksHost2)); + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, allowedToBeDownHost3)); + + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, allowedToBeDownHost1, noRemarksHost6), allowedToBeDownHost1); + + // allowedToBeDownHost4 is in cluster-3, while allowedToBeDownHost1 is in cluster-4, so allowedToBeDownHost4 should be ordered + // before allowedToBeDownHost1. + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, allowedToBeDownHost1, noRemarksHost6, allowedToBeDownHost4), + allowedToBeDownHost4, allowedToBeDownHost1); + + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, allowedToBeDownHost1, allowedToBeDownHost4, allowedToBeDownHost7), + allowedToBeDownHost7, allowedToBeDownHost4, allowedToBeDownHost1); + + verifyStorageNodesAllowedToBeDown( + modelUtils.createApplicationApiImpl(applicationInstance, allowedToBeDownHost4, allowedToBeDownHost1, allowedToBeDownHost7), + allowedToBeDownHost7, allowedToBeDownHost4, allowedToBeDownHost1); + } + + private void verifyStorageNodesAllowedToBeDown( + ApplicationApi applicationApi, HostName... hostNames) { + List<HostName> actualStorageNodes = + applicationApi.getStorageNodesAllowedToBeDownInGroupInReverseClusterOrder().stream() + .map(storageNode -> storageNode.hostName()) + .collect(Collectors.toList()); + assertEquals(Arrays.asList(hostNames), actualStorageNodes); + } +} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java new file mode 100644 index 00000000000..082002b7cf2 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ClusterApiImplTest.java @@ -0,0 +1,133 @@ +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.ServiceCluster; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.orchestrator.status.HostStatus; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Optional; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ClusterApiImplTest { + final ModelTestUtils modelUtils = new ModelTestUtils(); + + @Test + public void testServicesDownAndNotInGroup() { + HostName hostName1 = modelUtils.createNode("host1", HostStatus.NO_REMARKS); + HostName hostName2 = modelUtils.createNode("host2", HostStatus.NO_REMARKS); + HostName hostName3 = modelUtils.createNode("host3", HostStatus.ALLOWED_TO_BE_DOWN); + HostName hostName4 = modelUtils.createNode("host4", HostStatus.ALLOWED_TO_BE_DOWN); + HostName hostName5 = modelUtils.createNode("host5", HostStatus.NO_REMARKS); + + + ServiceCluster<ServiceMonitorStatus> serviceCluster = modelUtils.createServiceCluster( + "cluster", + new ServiceType("service-type"), + Arrays.asList( + modelUtils.createServiceInstance("service-1", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("service-2", hostName2, ServiceMonitorStatus.DOWN), + modelUtils.createServiceInstance("service-3", hostName3, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("service-4", hostName4, ServiceMonitorStatus.DOWN), + modelUtils.createServiceInstance("service-5", hostName5, ServiceMonitorStatus.UP) + ) + ); + + ClusterApiImpl clusterApi = new ClusterApiImpl( + serviceCluster, + new NodeGroup(modelUtils.createApplicationInstance(new ArrayList<>()), hostName5), + modelUtils.getHostStatusMap(), + modelUtils.getClusterControllerClientFactory()); + + assertEquals("{ clusterId=cluster, serviceType=service-type }", clusterApi.clusterInfo()); + assertFalse(clusterApi.isStorageCluster()); + assertEquals("[ServiceInstance{configId=service-2, hostName=host2, serviceStatus=DOWN}, " + + "ServiceInstance{configId=service-3, hostName=host3, serviceStatus=UP}, " + + "ServiceInstance{configId=service-4, hostName=host4, serviceStatus=DOWN}]", + clusterApi.servicesDownAndNotInGroupDescription()); + assertEquals("[host3, host4]", + clusterApi.nodesAllowedToBeDownNotInGroupDescription()); + assertEquals(60, clusterApi.percentageOfServicesDown()); + assertEquals(80, clusterApi.percentageOfServicesDownIfGroupIsAllowedToBeDown()); + } + + @Test + public void testNoServices() { + HostName hostName1 = modelUtils.createNode("host1", HostStatus.NO_REMARKS); + HostName hostName2 = modelUtils.createNode("host2", HostStatus.NO_REMARKS); + HostName hostName3 = modelUtils.createNode("host3", HostStatus.ALLOWED_TO_BE_DOWN); + HostName hostName4 = modelUtils.createNode("host4", HostStatus.ALLOWED_TO_BE_DOWN); + HostName hostName5 = modelUtils.createNode("host5", HostStatus.NO_REMARKS); + + + ServiceCluster<ServiceMonitorStatus> serviceCluster = modelUtils.createServiceCluster( + "cluster", + new ServiceType("service-type"), + Arrays.asList( + modelUtils.createServiceInstance("service-1", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("service-2", hostName2, ServiceMonitorStatus.DOWN), + modelUtils.createServiceInstance("service-3", hostName3, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("service-4", hostName4, ServiceMonitorStatus.DOWN), + modelUtils.createServiceInstance("service-5", hostName5, ServiceMonitorStatus.UP) + ) + ); + + verifyNoServices(serviceCluster, false, false, hostName1); + verifyNoServices(serviceCluster, true, false, hostName2); + verifyNoServices(serviceCluster, true, false, hostName3); + verifyNoServices(serviceCluster, true, false, hostName4); + verifyNoServices(serviceCluster, false, false, hostName5); + + verifyNoServices(serviceCluster, false, false, hostName1, hostName2); + verifyNoServices(serviceCluster, true, false, hostName2, hostName3); + verifyNoServices(serviceCluster, true, true, hostName2, hostName3, hostName4); + verifyNoServices(serviceCluster, false, true, hostName1, hostName2, hostName3, hostName4); + } + + private void verifyNoServices(ServiceCluster<ServiceMonitorStatus> serviceCluster, + boolean expectedNoServicesInGroupIsUp, + boolean expectedNoServicesOutsideGroupIsDown, + HostName... groupNodes) { + ClusterApiImpl clusterApi = new ClusterApiImpl( + serviceCluster, + new NodeGroup(modelUtils.createApplicationInstance(new ArrayList<>()), groupNodes), + modelUtils.getHostStatusMap(), + modelUtils.getClusterControllerClientFactory()); + + assertEquals(expectedNoServicesInGroupIsUp, clusterApi.noServicesInGroupIsUp()); + assertEquals(expectedNoServicesOutsideGroupIsDown, clusterApi.noServicesOutsideGroupIsDown()); + } + + @Test + public void testStorageCluster() { + HostName hostName1 = new HostName("host1"); + HostName hostName2 = new HostName("host2"); + HostName hostName3 = new HostName("host3"); + + ServiceCluster<ServiceMonitorStatus> serviceCluster = modelUtils.createServiceCluster( + "cluster", + VespaModelUtil.STORAGENODE_SERVICE_TYPE, + Arrays.asList( + modelUtils.createServiceInstance("storage-1", hostName1, ServiceMonitorStatus.UP), + modelUtils.createServiceInstance("storage-2", hostName2, ServiceMonitorStatus.DOWN) + ) + ); + + ClusterApiImpl clusterApi = new ClusterApiImpl( + serviceCluster, + new NodeGroup(modelUtils.createApplicationInstance(new ArrayList<>()), hostName1, hostName3), + new HashMap<>(), + modelUtils.getClusterControllerClientFactory()); + + assertTrue(clusterApi.isStorageCluster()); + assertEquals(Optional.of(hostName1), clusterApi.storageNodeInGroup().map(storageNode -> storageNode.hostName())); + assertEquals(Optional.of(hostName1), clusterApi.upStorageNodeInGroup().map(storageNode -> storageNode.hostName())); + } +}
\ No newline at end of file diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ModelTestUtils.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ModelTestUtils.java new file mode 100644 index 00000000000..03eb0184a88 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/ModelTestUtils.java @@ -0,0 +1,92 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.orchestrator.model; + +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.ServiceInstance; +import com.yahoo.vespa.applicationmodel.ServiceType; +import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; +import com.yahoo.vespa.orchestrator.status.HostStatus; +import com.yahoo.vespa.orchestrator.status.MutableStatusRegistry; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ModelTestUtils { + private final MutableStatusRegistry statusRegistry = mock(MutableStatusRegistry.class); + private final ClusterControllerClientFactory clusterControllerClientFactory = mock(ClusterControllerClientFactory.class); + private final Map<HostName, HostStatus> hostStatusMap = new HashMap<>(); + + ModelTestUtils() { + when(statusRegistry.getHostStatus(any())).thenReturn(HostStatus.NO_REMARKS); + } + + Map<HostName, HostStatus> getHostStatusMap() { + return hostStatusMap; + } + + HostName createNode(String name, HostStatus hostStatus) { + HostName hostName = new HostName(name); + hostStatusMap.put(hostName, hostStatus); + when(statusRegistry.getHostStatus(hostName)).thenReturn(hostStatus); + return hostName; + } + + ApplicationApiImpl createApplicationApiImpl( + ApplicationInstance<ServiceMonitorStatus> applicationInstance, + HostName... hostnames) { + NodeGroup nodeGroup = new NodeGroup(applicationInstance, hostnames); + return new ApplicationApiImpl(nodeGroup, statusRegistry, clusterControllerClientFactory); + } + + ApplicationInstance<ServiceMonitorStatus> createApplicationInstance( + List<ServiceCluster<ServiceMonitorStatus>> serviceClusters) { + Set<ServiceCluster<ServiceMonitorStatus>> serviceClusterSet = serviceClusters.stream() + .collect(Collectors.toSet()); + + return new ApplicationInstance<>( + new TenantId("tenant"), + new ApplicationInstanceId("application-name:foo:bar:default"), + serviceClusterSet); + } + + ServiceCluster<ServiceMonitorStatus> createServiceCluster( + String clusterId, + ServiceType serviceType, + List<ServiceInstance<ServiceMonitorStatus>> serviceInstances) { + Set<ServiceInstance<ServiceMonitorStatus>> serviceInstanceSet = serviceInstances.stream() + .collect(Collectors.toSet()); + + return new ServiceCluster<>( + new ClusterId(clusterId), + serviceType, + serviceInstanceSet); + } + + ServiceInstance<ServiceMonitorStatus> createServiceInstance( + String configId, + HostName hostName, + ServiceMonitorStatus status) { + return new ServiceInstance<>( + new ConfigId(configId), + hostName, + status); + } + + public ClusterControllerClientFactory getClusterControllerClientFactory() { + return clusterControllerClientFactory; + } +} diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/NodeGroupTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/NodeGroupTest.java new file mode 100644 index 00000000000..56a9e6bf147 --- /dev/null +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/NodeGroupTest.java @@ -0,0 +1,33 @@ +package com.yahoo.vespa.orchestrator.model; + +import com.yahoo.vespa.applicationmodel.ApplicationInstance; +import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; +import com.yahoo.vespa.applicationmodel.HostName; +import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashSet; + +import static org.junit.Assert.assertEquals; + +public class NodeGroupTest { + @Test + public void testBasics() { + ApplicationInstance<ServiceMonitorStatus> applicationInstance = new ApplicationInstance<>( + new TenantId("tenant"), + new ApplicationInstanceId("application-instance"), + new HashSet<>()); + + HostName hostName1 = new HostName("host1"); + HostName hostName2 = new HostName("host2"); + HostName hostName3 = new HostName("host3"); + NodeGroup nodeGroup = new NodeGroup(applicationInstance, hostName1, hostName3); + nodeGroup.addNode(hostName2); + + // hostnames are sorted (for no good reason other than testability due to stability, readability) + assertEquals(Arrays.asList(hostName1, hostName2, hostName3), nodeGroup.getHostNames()); + assertEquals("host1,host2,host3", nodeGroup.toCommaSeparatedString()); + } +}
\ No newline at end of file diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/VespaModelUtilTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/VespaModelUtilTest.java index c39410e9ca6..f2c17299d39 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/VespaModelUtilTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/model/VespaModelUtilTest.java @@ -1,5 +1,5 @@ // Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.orchestrator; +package com.yahoo.vespa.orchestrator.model; import com.yahoo.vespa.applicationmodel.ApplicationInstance; import com.yahoo.vespa.applicationmodel.ApplicationInstanceId; @@ -10,14 +10,15 @@ import com.yahoo.vespa.applicationmodel.ServiceCluster; import com.yahoo.vespa.applicationmodel.ServiceInstance; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.applicationmodel.TenantId; +import com.yahoo.vespa.orchestrator.TestUtil; import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; import org.junit.Test; +import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Optional; -import java.util.Set; -import com.google.common.collect.ImmutableSet; import static com.yahoo.vespa.orchestrator.TestUtil.makeServiceClusterSet; import static com.yahoo.vespa.orchestrator.TestUtil.makeServiceInstanceSet; import static junit.framework.TestCase.assertFalse; @@ -169,9 +170,8 @@ public class VespaModelUtilTest { @Test public void testGettingClusterControllerInstances() { - Set<ServiceInstance<?>> controllers = - new HashSet<>(VespaModelUtil.getClusterControllerInstances(application, CONTENT_CLUSTER_ID)); - Set<ServiceInstance<ServiceMonitorStatus>> expectedControllers = ImmutableSet.of(controller0, controller1); + List<HostName> controllers = VespaModelUtil.getClusterControllerInstancesInOrder(application, CONTENT_CLUSTER_ID); + List<HostName> expectedControllers = Arrays.asList(controller0.hostName(), controller1.hostName()); assertThat(controllers).isEqualTo(expectedControllers); } diff --git a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicyTest.java b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicyTest.java index 45d605b6d8a..09471f9d6ae 100644 --- a/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicyTest.java +++ b/orchestrator/src/test/java/com/yahoo/vespa/orchestrator/policy/HostedVespaPolicyTest.java @@ -12,19 +12,21 @@ import com.yahoo.vespa.applicationmodel.ServiceInstance; import com.yahoo.vespa.applicationmodel.ServiceType; import com.yahoo.vespa.applicationmodel.TenantId; import com.yahoo.vespa.orchestrator.TestUtil; -import com.yahoo.vespa.orchestrator.VespaModelUtil; import com.yahoo.vespa.orchestrator.controller.ClusterControllerClient; import com.yahoo.vespa.orchestrator.controller.ClusterControllerClientFactory; import com.yahoo.vespa.orchestrator.controller.ClusterControllerState; import com.yahoo.vespa.orchestrator.controller.ClusterControllerStateResponse; +import com.yahoo.vespa.orchestrator.model.VespaModelUtil; import com.yahoo.vespa.orchestrator.status.HostStatus; import com.yahoo.vespa.orchestrator.status.MutableStatusRegistry; import com.yahoo.vespa.service.monitor.ServiceMonitorStatus; import org.junit.Test; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import static com.yahoo.vespa.orchestrator.TestUtil.makeServiceClusterSet; import static com.yahoo.vespa.orchestrator.TestUtil.makeServiceInstanceSet; @@ -417,10 +419,12 @@ public class HostedVespaPolicyTest { // Verification phase. if (expectedNodeStateSentToClusterController.isPresent()) { + List<HostName> clusterControllers = CLUSTER_CONTROLLER_SERVICE_CLUSTER.serviceInstances().stream() + .map(service -> service.hostName()) + .collect(Collectors.toList()); + verify(clusterControllerClientFactory, times(1)) - .createClient( - CLUSTER_CONTROLLER_SERVICE_CLUSTER.serviceInstances(), - CONTENT_CLUSTER_NAME); + .createClient(clusterControllers, CONTENT_CLUSTER_NAME); verify(client, times(1)) .setNodeState( STORAGE_NODE_INDEX, |