diff options
32 files changed, 295 insertions, 170 deletions
diff --git a/clustercontroller-core/src/main/java/com/yahoo/vespa/clustercontroller/core/FleetControllerOptions.java b/clustercontroller-core/src/main/java/com/yahoo/vespa/clustercontroller/core/FleetControllerOptions.java index a088b50f078..0ad0a59a72a 100644 --- a/clustercontroller-core/src/main/java/com/yahoo/vespa/clustercontroller/core/FleetControllerOptions.java +++ b/clustercontroller-core/src/main/java/com/yahoo/vespa/clustercontroller/core/FleetControllerOptions.java @@ -16,6 +16,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import java.util.stream.Collectors; /** * This class represents all the options that can be set in the fleetcontroller. @@ -249,6 +250,12 @@ public class FleetControllerOptions implements Cloneable { sb.append("<tr><td><nobr>Max deferred task version wait time</nobr></td><td align=\"right\">").append(maxDeferredTaskVersionWaitTime.toMillis()).append("ms</td></tr>"); sb.append("<tr><td><nobr>Cluster has global document types configured</nobr></td><td align=\"right\">").append(clusterHasGlobalDocumentTypes).append("</td></tr>"); sb.append("<tr><td><nobr>Enable 2-phase cluster state activation protocol</nobr></td><td align=\"right\">").append(enableTwoPhaseClusterStateActivation).append("</td></tr>"); + sb.append("<tr><td><nobr>Cluster auto feed block on resource exhaustion enabled</nobr></td><td align=\"right\">") + .append(clusterFeedBlockEnabled).append("</td></tr>"); + sb.append("<tr><td><nobr>Feed block limits</nobr></td><td align=\"right\">") + .append(clusterFeedBlockLimit.entrySet().stream() + .map(kv -> String.format("%s: %.2f%%", kv.getKey(), kv.getValue() * 100.0)) + .collect(Collectors.joining("<br/>"))).append("</td></tr>"); sb.append("</table>"); } diff --git a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java index bc131b27080..a5dc3f21a1d 100644 --- a/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java +++ b/config-model-api/src/main/java/com/yahoo/config/model/api/ModelContext.java @@ -76,7 +76,6 @@ public interface ModelContext { @ModelFeatureFlag(owners = {"baldersheim"}) default boolean skipMbusReplyThread() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"tokle"}) default boolean useAccessControlTlsHandshakeClientAuth() { return false; } @ModelFeatureFlag(owners = {"baldersheim"}) default boolean useAsyncMessageHandlingOnSchedule() { throw new UnsupportedOperationException("TODO specify default value"); } - @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter = "7.348") default int contentNodeBucketDBStripeBits() { return 4; } @ModelFeatureFlag(owners = {"baldersheim"}, removeAfter = "7.350") default int mergeChunkSize() { return 0x2000000; } @ModelFeatureFlag(owners = {"baldersheim"}) default double feedConcurrency() { throw new UnsupportedOperationException("TODO specify default value"); } @ModelFeatureFlag(owners = {"baldersheim"}) default boolean useBucketExecutorForLidSpaceCompact() { throw new UnsupportedOperationException("TODO specify default value"); } diff --git a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java index 14658e57c1b..f4b79129ada 100644 --- a/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java +++ b/config-provisioning/src/main/java/com/yahoo/config/provision/ClusterSpec.java @@ -77,10 +77,8 @@ public final class ClusterSpec { */ public boolean isExclusive() { return exclusive; } - /** Whether this cluster has state */ - public boolean isStateful() { - return stateful; - } + /** Returns whether this cluster has state */ + public boolean isStateful() { return stateful; } public ClusterSpec with(Optional<Group> newGroup) { return new ClusterSpec(type, id, newGroup, vespaVersion, exclusive, combinedId, dockerImageRepo, stateful); diff --git a/document/src/main/java/com/yahoo/document/DocumentPut.java b/document/src/main/java/com/yahoo/document/DocumentPut.java index e24388cd65f..25246dc9a9e 100644 --- a/document/src/main/java/com/yahoo/document/DocumentPut.java +++ b/document/src/main/java/com/yahoo/document/DocumentPut.java @@ -54,7 +54,7 @@ public class DocumentPut extends DocumentOperation { if (o == null || getClass() != o.getClass()) return false; DocumentPut that = (DocumentPut) o; return document.equals(that.document) && - getCondition().equals(that.getCondition()); + Objects.equals(getCondition(), that.getCondition()); } @Override diff --git a/document/src/main/java/com/yahoo/document/DocumentRemove.java b/document/src/main/java/com/yahoo/document/DocumentRemove.java index 79f80713c44..a815d9c0a5a 100644 --- a/document/src/main/java/com/yahoo/document/DocumentRemove.java +++ b/document/src/main/java/com/yahoo/document/DocumentRemove.java @@ -1,6 +1,8 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.document; +import java.util.Objects; + /** * @author baldersheim */ @@ -21,9 +23,10 @@ public class DocumentRemove extends DocumentOperation { @Override public boolean equals(Object o) { if (this == o) return true; - if (!(o instanceof DocumentRemove)) return false; + if ( ! (o instanceof DocumentRemove)) return false; DocumentRemove that = (DocumentRemove) o; - if (!docId.equals(that.docId)) return false; + if ( ! docId.equals(that.docId)) return false; + if ( ! Objects.equals(getCondition(), that.getCondition())) return false; return true; } diff --git a/document/src/main/java/com/yahoo/document/DocumentUpdate.java b/document/src/main/java/com/yahoo/document/DocumentUpdate.java index 5c748f48f15..cba51ee999e 100644 --- a/document/src/main/java/com/yahoo/document/DocumentUpdate.java +++ b/document/src/main/java/com/yahoo/document/DocumentUpdate.java @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; /** @@ -362,9 +363,10 @@ public class DocumentUpdate extends DocumentOperation implements Iterable<FieldP if (docId != null ? !docId.equals(that.docId) : that.docId != null) return false; if (documentType != null ? !documentType.equals(that.documentType) : that.documentType != null) return false; - if (!fieldPathUpdates.equals(that.fieldPathUpdates)) return false; - if (!id2FieldUpdates.equals(that.id2FieldUpdates)) return false; - if (this.getCreateIfNonExistent() != ((DocumentUpdate) o).getCreateIfNonExistent()) return false; + if ( ! fieldPathUpdates.equals(that.fieldPathUpdates)) return false; + if ( ! id2FieldUpdates.equals(that.id2FieldUpdates)) return false; + if (this.getCreateIfNonExistent() != that.getCreateIfNonExistent()) return false; + if ( ! Objects.equals(getCondition(), that.getCondition())) return false; return true; } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java index b0b61e8a6b2..19c1fa090c9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeList.java @@ -69,6 +69,16 @@ public class NodeList extends AbstractFilteringList<Node, NodeList> { return matching(node -> node.allocation().isPresent() && node.allocation().get().membership().cluster().type().isContainer()); } + /** Returns the subset of nodes that run a stateless service */ + public NodeList stateless() { + return matching(node -> node.allocation().isPresent() && ! node.allocation().get().membership().cluster().isStateful()); + } + + /** Returns the subset of nodes that run a stateful service */ + public NodeList stateful() { + return matching(node -> node.allocation().isPresent() && node.allocation().get().membership().cluster().isStateful()); + } + /** Returns the subset of nodes that are currently changing their Vespa version */ public NodeList changingVersion() { return matching(node -> node.status().vespaVersion().isPresent() && diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java index 4eb38fa650e..8e14b61db9a 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java @@ -29,6 +29,7 @@ import com.yahoo.vespa.hosted.provision.maintenance.NodeFailer; import com.yahoo.vespa.hosted.provision.maintenance.PeriodicApplicationMaintainer; import com.yahoo.vespa.hosted.provision.node.Agent; import com.yahoo.vespa.hosted.provision.node.Allocation; +import com.yahoo.vespa.hosted.provision.node.History; import com.yahoo.vespa.hosted.provision.node.IP; import com.yahoo.vespa.hosted.provision.node.NodeAcl; import com.yahoo.vespa.hosted.provision.node.filter.NodeFilter; @@ -410,7 +411,7 @@ public class NodeRepository extends AbstractComponent { for (Node node : nodes) { if ( ! node.flavor().getType().equals(Flavor.Type.DOCKER_CONTAINER)) illegal("Cannot add " + node + ": This is not a docker node"); - if ( ! node.allocation().isPresent()) + if (node.allocation().isEmpty()) illegal("Cannot add " + node + ": Docker containers needs to be allocated"); Optional<Node> existing = getNode(node.hostname()); if (existing.isPresent()) @@ -514,7 +515,13 @@ public class NodeRepository extends AbstractComponent { * transaction commits. */ public List<Node> deactivate(List<Node> nodes, ApplicationTransaction transaction) { - return db.writeTo(State.inactive, nodes, Agent.application, Optional.empty(), transaction.nested()); + var stateless = NodeList.copyOf(nodes).stateless(); + var stateful = NodeList.copyOf(nodes).stateful(); + List<Node> written = new ArrayList<>(); + written.addAll(deallocate(stateless.asList(), Agent.application, "Deactivated by application", transaction.nested())); + written.addAll(db.writeTo(State.inactive, stateful.asList(), Agent.application, Optional.empty(), transaction.nested())); + return written; + } /** Removes this application: Active nodes are deactivated while all non-active nodes are set dirty. */ @@ -531,22 +538,11 @@ public class NodeRepository extends AbstractComponent { } /** Move nodes to the dirty state */ - public List<Node> setDirty(List<Node> nodes, Agent agent, String reason) { - return performOn(NodeListFilter.from(nodes), (node, lock) -> setDirty(node, agent, reason)); - } - - /** - * Set a node dirty, allowed if it is in the provisioned, inactive, failed or parked state. - * Use this to clean newly provisioned nodes or to recycle failed nodes which have been repaired or put on hold. - * - * @throws IllegalArgumentException if the node has hardware failure - */ - public Node setDirty(Node node, Agent agent, String reason) { - return db.writeTo(State.dirty, node, agent, Optional.of(reason)); + public List<Node> deallocate(List<Node> nodes, Agent agent, String reason) { + return performOn(NodeListFilter.from(nodes), (node, lock) -> deallocate(node, agent, reason)); } - - public List<Node> dirtyRecursively(String hostname, Agent agent, String reason) { + public List<Node> deallocateRecursively(String hostname, Agent agent, String reason) { Node nodeToDirty = getNode(hostname).orElseThrow(() -> new IllegalArgumentException("Could not deallocate " + hostname + ": Node not found")); @@ -568,7 +564,37 @@ public class NodeRepository extends AbstractComponent { illegal("Could not deallocate " + nodeToDirty + ": " + hostnamesNotAllowedToDirty + " are not in states [provisioned, failed, parked, breakfixed]"); - return nodesToDirty.stream().map(node -> setDirty(node, agent, reason)).collect(Collectors.toList()); + return nodesToDirty.stream().map(node -> deallocate(node, agent, reason)).collect(Collectors.toList()); + } + + /** + * Set a node dirty or parked, allowed if it is in the provisioned, inactive, failed or parked state. + * Use this to clean newly provisioned nodes or to recycle failed nodes which have been repaired or put on hold. + */ + public Node deallocate(Node node, Agent agent, String reason) { + NestedTransaction transaction = new NestedTransaction(); + Node deallocated = deallocate(node, agent, reason, transaction); + transaction.commit(); + return deallocated; + } + + public List<Node> deallocate(List<Node> nodes, Agent agent, String reason, NestedTransaction transaction) { + return nodes.stream().map(node -> deallocate(node, agent, reason, transaction)).collect(Collectors.toList()); + } + + public Node deallocate(Node node, Agent agent, String reason, NestedTransaction transaction) { + if (node.state() != State.parked && agent != Agent.operator + && (node.status().wantToDeprovision() || retiredByOperator(node))) + return park(node.hostname(), false, agent, reason, transaction); + else + return db.writeTo(State.dirty, List.of(node), agent, Optional.of(reason), transaction).get(0); + } + + private static boolean retiredByOperator(Node node) { + return node.status().wantToRetire() && node.history().event(History.Event.Type.wantToRetire) + .map(History.Event::agent) + .map(agent -> agent == Agent.operator) + .orElse(false); } /** @@ -597,7 +623,14 @@ public class NodeRepository extends AbstractComponent { * @throws NoSuchNodeException if the node is not found */ public Node park(String hostname, boolean keepAllocation, Agent agent, String reason) { - return move(hostname, keepAllocation, State.parked, agent, Optional.of(reason)); + NestedTransaction transaction = new NestedTransaction(); + Node parked = park(hostname, keepAllocation, agent, reason, transaction); + transaction.commit(); + return parked; + } + + public Node park(String hostname, boolean keepAllocation, Agent agent, String reason, NestedTransaction transaction) { + return move(hostname, keepAllocation, State.parked, agent, Optional.of(reason), transaction); } /** @@ -644,6 +677,14 @@ public class NodeRepository extends AbstractComponent { } private Node move(String hostname, boolean keepAllocation, State toState, Agent agent, Optional<String> reason) { + NestedTransaction transaction = new NestedTransaction(); + Node moved = move(hostname, keepAllocation, toState, agent, reason, transaction); + transaction.commit(); + return moved; + } + + private Node move(String hostname, boolean keepAllocation, State toState, Agent agent, Optional<String> reason, + NestedTransaction transaction) { Node node = getNode(hostname).orElseThrow(() -> new NoSuchNodeException("Could not move " + hostname + " to " + toState + ": Node not found")); @@ -651,10 +692,17 @@ public class NodeRepository extends AbstractComponent { node = node.withoutAllocation(); } - return move(node, toState, agent, reason); + return move(node, toState, agent, reason, transaction); } private Node move(Node node, State toState, Agent agent, Optional<String> reason) { + NestedTransaction transaction = new NestedTransaction(); + Node moved = move(node, toState, agent, reason, transaction); + transaction.commit(); + return moved; + } + + private Node move(Node node, State toState, Agent agent, Optional<String> reason, NestedTransaction transaction) { if (toState == Node.State.active && node.allocation().isEmpty()) illegal("Could not set " + node + " active. It has no allocation."); @@ -667,7 +715,7 @@ public class NodeRepository extends AbstractComponent { illegal("Could not set " + node + " active: Same cluster and index as " + currentActive); } } - return db.writeTo(toState, node, agent, reason); + return db.writeTo(toState, List.of(node), agent, reason, transaction).get(0); } } @@ -981,4 +1029,5 @@ public class NodeRepository extends AbstractComponent { private void illegal(String message) { throw new IllegalArgumentException(message); } + } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirer.java index b55f49722cb..1a47af6b929 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirer.java @@ -112,7 +112,7 @@ public class FailedExpirer extends NodeRepositoryMaintainer { nodesToRecycle.add(candidate); } } - nodeRepository.setDirty(nodesToRecycle, Agent.FailedExpirer, "Expired by FailedExpirer"); + nodeRepository.deallocate(nodesToRecycle, Agent.FailedExpirer, "Expired by FailedExpirer"); } /** Returns whether the current node fail count should be used as an indicator of hardware issue */ diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/InactiveExpirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/InactiveExpirer.java index 392146f6ead..231d2ac08b1 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/InactiveExpirer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/InactiveExpirer.java @@ -14,14 +14,9 @@ import java.util.List; /** * Maintenance job which moves inactive nodes to dirty or parked after timeout. * - * The timeout is in place for two reasons: - * - * - To ensure that the new application configuration has time to - * propagate before the node is used for something else. - * - * - To provide a grace period in which nodes can be brought back to active - * if they were deactivated in error. As inactive nodes retain their state - * they can be brought back to active and correct state faster than a new node. + * The timeout is in place to provide a grace period in which nodes can be brought back to active + * if they were deactivated in error. As inactive nodes retain their state + * they can be brought back to active and correct state faster than a new node. * * Nodes with following flags set are not reusable and will be moved to parked * instead of dirty: @@ -44,11 +39,7 @@ public class InactiveExpirer extends Expirer { @Override protected void expire(List<Node> expired) { expired.forEach(node -> { - if (node.status().wantToDeprovision() || retiredByOperator(node)) { - nodeRepository.park(node.hostname(), false, Agent.InactiveExpirer, "Expired by InactiveExpirer"); - } else { - nodeRepository.setDirty(node, Agent.InactiveExpirer, "Expired by InactiveExpirer"); - } + nodeRepository.deallocate(node, Agent.InactiveExpirer, "Expired by InactiveExpirer"); }); } @@ -58,11 +49,4 @@ public class InactiveExpirer extends Expirer { || node.allocation().get().owner().instance().isTester(); } - private static boolean retiredByOperator(Node node) { - return node.status().wantToRetire() && node.history().event(History.Event.Type.wantToRetire) - .map(History.Event::agent) - .map(agent -> agent == Agent.operator) - .orElse(false); - } - } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java index 66ddf7f9ffe..52c487c28cf 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/MaintenanceDeployment.java @@ -186,7 +186,7 @@ class MaintenanceDeployment implements Closeable { // Immediately clean up if we reserved the node but could not activate or reserved a node on the wrong host expectedNewNode.flatMap(node -> nodeRepository.getNode(node.hostname(), Node.State.reserved)) - .ifPresent(node -> nodeRepository.setDirty(node, agent, "Expired by " + agent)); + .ifPresent(node -> nodeRepository.deallocate(node, agent, "Expired by " + agent)); } } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirer.java index c6427123d09..1967615de02 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirer.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/maintenance/ReservationExpirer.java @@ -25,6 +25,6 @@ public class ReservationExpirer extends Expirer { } @Override - protected void expire(List<Node> expired) { nodeRepository().setDirty(expired, Agent.ReservationExpirer, "Expired by ReservationExpirer"); } + protected void expire(List<Node> expired) { nodeRepository().deallocate(expired, Agent.ReservationExpirer, "Expired by ReservationExpirer"); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java index 9b5dac49e32..08bfd104863 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/NodesV2ApiHandler.java @@ -142,7 +142,7 @@ public class NodesV2ApiHandler extends LoggingRequestHandler { return new MessageResponse("Moved " + hostnamesAsString(parkedNodes) + " to parked"); } else if (path.startsWith("/nodes/v2/state/dirty/")) { - List<Node> dirtiedNodes = nodeRepository.dirtyRecursively(lastElement(path), Agent.operator, "Dirtied through the nodes/v2 API"); + List<Node> dirtiedNodes = nodeRepository.deallocateRecursively(lastElement(path), Agent.operator, "Dirtied through the nodes/v2 API"); return new MessageResponse("Moved " + hostnamesAsString(dirtiedNodes) + " to dirty"); } else if (path.startsWith("/nodes/v2/state/active/")) { diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java index 1f8f3d32043..cf1ccf9a052 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/testutils/MockNodeRepository.java @@ -142,11 +142,11 @@ public class MockNodeRepository extends NodeRepository { nodes = addNodes(nodes, Agent.system); nodes.remove(node7); nodes.remove(node55); - nodes = setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = deallocate(nodes, Agent.system, getClass().getSimpleName()); setReady(nodes, Agent.system, getClass().getSimpleName()); fail(node5.hostname(), Agent.system, getClass().getSimpleName()); - dirtyRecursively(node55.hostname(), Agent.system, getClass().getSimpleName()); + deallocateRecursively(node55.hostname(), Agent.system, getClass().getSimpleName()); fail("dockerhost6.yahoo.com", Agent.operator, getClass().getSimpleName()); removeRecursively("dockerhost6.yahoo.com"); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java index bcf53a07490..5f357b9e4b0 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/NodeRepositoryTest.java @@ -228,12 +228,12 @@ public class NodeRepositoryTest { assertEquals(6, tester.nodeRepository().getNodes().size()); // Should be OK to dirty host2 as it is in provisioned and its only child is in failed - tester.nodeRepository().dirtyRecursively("host2", Agent.system, NodeRepositoryTest.class.getSimpleName()); + tester.nodeRepository().deallocateRecursively("host2", Agent.system, NodeRepositoryTest.class.getSimpleName()); assertEquals(asSet("host2", "node20"), filterNodes(tester, node -> node.state() == Node.State.dirty)); // Cant dirty host1, node11 is ready and node12 is active try { - tester.nodeRepository().dirtyRecursively("host1", Agent.system, NodeRepositoryTest.class.getSimpleName()); + tester.nodeRepository().deallocateRecursively("host1", Agent.system, NodeRepositoryTest.class.getSimpleName()); fail("Should not be able to dirty host1"); } catch (IllegalArgumentException ignored) { } // Expected; diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java index 940404fa605..9f2f7541c91 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/FailedExpirerTest.java @@ -312,7 +312,7 @@ public class FailedExpirerTest { List<Node> nodes = Stream.of(hostname) .map(this::get) .collect(Collectors.toList()); - nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = nodeRepository.deallocate(nodes, Agent.system, getClass().getSimpleName()); nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); return this; } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirerTest.java index 8bcf1552204..60ca625d07e 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/LoadBalancerExpirerTest.java @@ -60,12 +60,6 @@ public class LoadBalancerExpirerTest { // Expirer defers removal while nodes are still allocated to application expirer.maintain(); - assertEquals(3, tester.loadBalancerService().instances().size()); - assertEquals(2, tester.loadBalancerService().instances().get(lb1).reals().size()); - dirtyNodesOf(app1, cluster1); - - // Expirer prunes reals before expiration time of load balancer itself - expirer.maintain(); assertEquals(Set.of(), tester.loadBalancerService().instances().get(lb1).reals()); assertEquals(Set.of(), loadBalancers.get().get(lb1).instance().reals()); @@ -138,11 +132,11 @@ public class LoadBalancerExpirerTest { } private void dirtyNodesOf(ApplicationId application, ClusterSpec.Id cluster) { - tester.nodeRepository().setDirty(tester.nodeRepository().getNodes(application).stream() - .filter(node -> node.allocation().isPresent()) - .filter(node -> node.allocation().get().membership().cluster().id().equals(cluster)) - .collect(Collectors.toList()), - Agent.system, this.getClass().getSimpleName()); + tester.nodeRepository().deallocate(tester.nodeRepository().getNodes(application).stream() + .filter(node -> node.allocation().isPresent()) + .filter(node -> node.allocation().get().membership().cluster().id().equals(cluster)) + .collect(Collectors.toList()), + Agent.system, this.getClass().getSimpleName()); } private void deployApplication(ApplicationId application, ClusterSpec.Id... clusters) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java index c03e489def2..9adba744101 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/MetricsReporterTest.java @@ -163,7 +163,7 @@ public class MetricsReporterTest { Node dockerHost = Node.create("openStackId1", new IP.Config(Set.of("::1"), ipAddressPool), "dockerHost", nodeFlavors.getFlavorOrThrow("host"), NodeType.host).build(); nodeRepository.addNodes(List.of(dockerHost), Agent.system); - nodeRepository.dirtyRecursively("dockerHost", Agent.system, getClass().getSimpleName()); + nodeRepository.deallocateRecursively("dockerHost", Agent.system, getClass().getSimpleName()); nodeRepository.setReady("dockerHost", Agent.system, getClass().getSimpleName()); Node container1 = Node.createDockerNode(Set.of("::2"), "container1", @@ -225,7 +225,7 @@ public class MetricsReporterTest { ApplicationId application = ApplicationId.from("t1", "a1", "default"); Map<String, String> dimensions = Map.of("applicationId", application.toFullString()); NodeResources resources = new NodeResources(2, 8, 100, 1); - List<Node> activeNodes = tester.deploy(application, Capacity.from(new ClusterResources(4, 1, resources))); + List<Node> activeNodes = tester.deploy(application, ProvisioningTester.contentClusterSpec(), Capacity.from(new ClusterResources(4, 1, resources))); metricsReporter.maintain(); assertEquals(0D, getMetric("nodes.nonActiveFraction", metric, dimensions)); assertEquals(4, getMetric("nodes.active", metric, dimensions)); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java index 12b682ae26c..d0473d08ea2 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/NodeFailTester.java @@ -259,7 +259,7 @@ public class NodeFailTester { } nodes = nodeRepository.addNodes(nodes, Agent.system); - nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = nodeRepository.deallocate(nodes, Agent.system, getClass().getSimpleName()); return nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); } @@ -267,7 +267,7 @@ public class NodeFailTester { List<Node> nodes = tester.makeProvisionedNodes(count, (index) -> "parent" + index, hostFlavors.getFlavorOrThrow("default"), Optional.empty(), NodeType.host, 10, false); - nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = nodeRepository.deallocate(nodes, Agent.system, getClass().getSimpleName()); tester.activateTenantHosts(); return nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java index cd188bc017f..ae1e36d73dd 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/PeriodicApplicationMaintainerTest.java @@ -96,10 +96,10 @@ public class PeriodicApplicationMaintainerTest { // Cause maintenance deployment which will update the applications with the re-activated nodes clock.advance(Duration.ofMinutes(35)); // Otherwise redeploys are inhibited fixture.runApplicationMaintainer(); - assertEquals("Superflous content nodes are retired", + assertEquals("Superfluous content nodes are retired", reactivatedInApp2, fixture.getNodes(Node.State.active).retired().size()); - assertEquals("Superflous container nodes are deactivated (this makes little point for container nodes)", - reactivatedInApp1, fixture.getNodes(Node.State.inactive).size()); + assertEquals("Superfluous container nodes are deallocated", + reactivatedInApp1, fixture.getNodes(Node.State.dirty).size()); } @Test(timeout = 60_000) diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java index 0246ec524e3..ef0e6d4ddc4 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/DockerProvisioningTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.provision.provisioning; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; import com.yahoo.config.provision.ApplicationName; +import com.yahoo.config.provision.ApplicationTransaction; import com.yahoo.config.provision.Capacity; import com.yahoo.config.provision.ClusterResources; import com.yahoo.config.provision.ClusterSpec; @@ -14,11 +15,14 @@ import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.NodeResources; import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.OutOfCapacityException; +import com.yahoo.config.provision.ProvisionLock; import com.yahoo.config.provision.RegionName; import com.yahoo.config.provision.TenantName; import com.yahoo.config.provision.Zone; +import com.yahoo.transaction.NestedTransaction; import com.yahoo.vespa.hosted.provision.Node; import com.yahoo.vespa.hosted.provision.NodeList; +import com.yahoo.vespa.hosted.provision.node.Agent; import org.junit.Test; import java.util.HashSet; @@ -356,11 +360,15 @@ public class DockerProvisioningTest { tester.makeReadyHosts(5, r).activateTenantHosts(); ApplicationId app1 = ProvisioningTester.applicationId("app1"); - ClusterSpec cluster1 = ClusterSpec.request(ClusterSpec.Type.container, new ClusterSpec.Id("cluster1")).vespaVersion("7").build(); + ClusterSpec cluster1 = ClusterSpec.request(ClusterSpec.Type.content, new ClusterSpec.Id("cluster1")).vespaVersion("7").build(); tester.activate(app1, cluster1, Capacity.from(new ClusterResources(5, 1, r))); tester.activate(app1, cluster1, Capacity.from(new ClusterResources(2, 1, r))); + var tx = new ApplicationTransaction(new ProvisionLock(app1, tester.nodeRepository().lock(app1)), new NestedTransaction()); + tester.nodeRepository().deactivate(tester.nodeRepository().list(app1, Node.State.active).retired().asList(), tx); + tx.nested().commit(); + assertEquals(2, tester.getNodes(app1, Node.State.active).size()); assertEquals(3, tester.getNodes(app1, Node.State.inactive).size()); @@ -372,16 +380,16 @@ public class DockerProvisioningTest { } @Test - public void inactive_container_nodes_are_reused() { - assertInactiveReuse(ClusterSpec.Type.container); + public void inactive_container_nodes_are_not_reused() { + assertInactiveReuse(ClusterSpec.Type.container, false); } @Test public void inactive_content_nodes_are_reused() { - assertInactiveReuse(ClusterSpec.Type.content); + assertInactiveReuse(ClusterSpec.Type.content, true); } - private void assertInactiveReuse(ClusterSpec.Type clusterType) { + private void assertInactiveReuse(ClusterSpec.Type clusterType, boolean expectedReuse) { NodeResources r = new NodeResources(20, 40, 100, 4); ProvisioningTester tester = new ProvisioningTester.Builder().zone(new Zone(Environment.prod, RegionName.from("us-east"))) .flavors(List.of(new Flavor(r))) @@ -398,9 +406,18 @@ public class DockerProvisioningTest { tester.nodeRepository().setRemovable(app1, tester.getNodes(app1).retired().asList()); tester.activate(app1, cluster1, Capacity.from(new ClusterResources(2, 1, r))); - assertEquals(2, tester.getNodes(app1, Node.State.inactive).size()); - tester.activate(app1, cluster1, Capacity.from(new ClusterResources(4, 1, r))); - assertEquals(0, tester.getNodes(app1, Node.State.inactive).size()); + if (expectedReuse) { + assertEquals(2, tester.getNodes(app1, Node.State.inactive).size()); + tester.activate(app1, cluster1, Capacity.from(new ClusterResources(4, 1, r))); + assertEquals(0, tester.getNodes(app1, Node.State.inactive).size()); + } + else { + assertEquals(0, tester.getNodes(app1, Node.State.inactive).size()); + assertEquals(2, tester.nodeRepository().getNodes(Node.State.dirty).size()); + tester.nodeRepository().setReady(tester.nodeRepository().getNodes(Node.State.dirty), Agent.system, "test"); + tester.activate(app1, cluster1, Capacity.from(new ClusterResources(4, 1, r))); + } + } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java index eef342b527b..105f2122e0c 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/LoadBalancerProvisionerTest.java @@ -263,7 +263,7 @@ public class LoadBalancerProvisionerTest { } private void dirtyNodesOf(ApplicationId application) { - tester.nodeRepository().setDirty(tester.nodeRepository().getNodes(application), Agent.system, this.getClass().getSimpleName()); + tester.nodeRepository().deallocate(tester.nodeRepository().getNodes(application), Agent.system, this.getClass().getSimpleName()); } private Set<HostSpec> prepare(ApplicationId application, ClusterSpec... specs) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeTypeProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeTypeProvisioningTest.java index acf7bda3cbf..bf2a6ba3627 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeTypeProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/NodeTypeProvisioningTest.java @@ -153,7 +153,7 @@ public class NodeTypeProvisioningTest { assertEquals(10, nodes.size()); // Verify that the node is now inactive - assertEquals(Node.State.inactive, tester.nodeRepository().getNode(nodeToRetire.hostname()) + assertEquals(Node.State.dirty, tester.nodeRepository().getNode(nodeToRetire.hostname()) .orElseThrow(RuntimeException::new).state()); } } @@ -232,7 +232,7 @@ public class NodeTypeProvisioningTest { assertEquals(10, nodes.size()); // Verify the node we previously set to retire has finished retiring - assertEquals(Node.State.inactive, tester.nodeRepository().getNode(currentyRetiringHostname) + assertEquals(Node.State.dirty, tester.nodeRepository().getNode(currentyRetiringHostname) .orElseThrow(RuntimeException::new).state()); // Verify that a node is currently retiring @@ -263,9 +263,9 @@ public class NodeTypeProvisioningTest { .count(); assertEquals(11 - numNodesToRetire, numRetiredActiveProxyNodes); - // All the nodes that were marked with wantToRetire earlier are now inactive + // All the nodes that were marked with wantToRetire earlier are now dirty assertEquals(nodesToRetire.stream().map(Node::hostname).collect(Collectors.toSet()), - tester.nodeRepository().getNodes(Node.State.inactive).stream().map(Node::hostname).collect(Collectors.toSet())); + tester.nodeRepository().getNodes(Node.State.dirty).stream().map(Node::hostname).collect(Collectors.toSet())); } private List<HostSpec> deployProxies(ApplicationId application, ProvisioningTester tester) { diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java index 611c3839f56..7778077110a 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTest.java @@ -32,6 +32,7 @@ import com.yahoo.vespa.service.duper.InfraApplication; import org.junit.Test; import java.time.Duration; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -100,14 +101,15 @@ public class ProvisioningTest { SystemState state6 = prepare(application1, 0, 2, 0, 3, defaultResources, tester); tester.activate(application1, state6.allHosts); assertEquals(5, tester.getNodes(application1, Node.State.active).size()); - assertEquals(5, tester.getNodes(application1, Node.State.inactive).size()); + assertEquals(3, tester.getNodes(application1, Node.State.inactive).size()); // delete app NodeList previouslyActive = tester.getNodes(application1, Node.State.active); NodeList previouslyInactive = tester.getNodes(application1, Node.State.inactive); tester.remove(application1); - assertEquals(tester.toHostNames(previouslyActive.asList()), tester.toHostNames(tester.nodeRepository().getNodes(application1, Node.State.inactive))); - assertEquals(tester.toHostNames(previouslyInactive.asList()), tester.toHostNames(tester.nodeRepository().getNodes(Node.State.dirty))); + assertEquals(tester.toHostNames(previouslyActive.not().container().asList()), + tester.toHostNames(tester.nodeRepository().getNodes(application1, Node.State.inactive))); + assertTrue(tester.nodeRepository().getNodes(Node.State.dirty).containsAll(previouslyActive.container().asList())); assertEquals(0, tester.getNodes(application1, Node.State.active).size()); assertTrue(tester.nodeRepository().applications().get(application1).isEmpty()); @@ -203,7 +205,7 @@ public class ProvisioningTest { ApplicationId application1 = ProvisioningTester.applicationId(); - tester.makeReadyHosts(24, defaultResources); + tester.makeReadyHosts(30, defaultResources); tester.activateTenantHosts(); // deploy @@ -219,8 +221,8 @@ public class ProvisioningTest { // decrease again SystemState state3 = prepare(application1, 2, 2, 3, 3, defaultResources, tester); tester.activate(application1, state3.allHosts); - assertEquals("Superfluous container nodes are deactivated", - 3-2 + 4-2, tester.getNodes(application1, Node.State.inactive).size()); + assertEquals("Superfluous container nodes are dirtyed", + 3-2 + 4-2, tester.nodeRepository().getNodes(Node.State.dirty).size()); assertEquals("Superfluous content nodes are retired", 4-3 + 5-3, tester.getNodes(application1, Node.State.active).retired().size()); @@ -229,7 +231,6 @@ public class ProvisioningTest { assertEquals("Inactive nodes are reused", 0, tester.getNodes(application1, Node.State.inactive).size()); assertEquals("Earlier retired nodes are not unretired before activate", 4-3 + 5-3, tester.getNodes(application1, Node.State.active).retired().size()); - state4.assertExtends(state2); assertEquals("New and inactive nodes are reserved", 4 + 3, tester.getNodes(application1, Node.State.reserved).size()); // Remove a retired host from one of the content clusters (which one is random depending on host names) HostSpec removed = state4.removeHost(tester.getNodes(application1, Node.State.active).retired().asList().get(0).hostname()); @@ -243,8 +244,8 @@ public class ProvisioningTest { // decrease again SystemState state5 = prepare(application1, 2, 2, 3, 3, defaultResources, tester); tester.activate(application1, state5.allHosts); - assertEquals("Superfluous container nodes are also deactivated", - 4-2 + 5-2 + 1, tester.getNodes(application1, Node.State.inactive).size()); // + assertEquals("Superfluous container nodes are also dirtyed", + 4-2 + 5-2 + 1 + 4-2, tester.nodeRepository().getNodes(Node.State.dirty).size()); assertEquals("Superfluous content nodes are retired", 5-3 + 6-3 - 1, tester.getNodes(application1, Node.State.active).retired().size()); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java index ebd856e96a0..7c43a8d5859 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/provisioning/ProvisioningTester.java @@ -456,7 +456,7 @@ public class ProvisioningTester { } nodes = nodeRepository.addNodes(nodes, Agent.system); - nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = nodeRepository.deallocate(nodes, Agent.system, getClass().getSimpleName()); nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); ConfigServerApplication application = new ConfigServerApplication(); @@ -482,7 +482,7 @@ public class ProvisioningTester { } public List<Node> makeReadyNodes(int n, Flavor flavor, Optional<TenantName> reservedTo, NodeType type, int ipAddressPoolSize, boolean dualStack) { List<Node> nodes = makeProvisionedNodes(n, flavor, reservedTo, type, ipAddressPoolSize, dualStack); - nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = nodeRepository.deallocate(nodes, Agent.system, getClass().getSimpleName()); return nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); } @@ -523,7 +523,7 @@ public class ProvisioningTester { nodes.add(builder.build()); } nodes = nodeRepository.addNodes(nodes, Agent.system); - nodes = nodeRepository.setDirty(nodes, Agent.system, getClass().getSimpleName()); + nodes = nodeRepository.deallocate(nodes, Agent.system, getClass().getSimpleName()); nodeRepository.setReady(nodes, Agent.system, getClass().getSimpleName()); return nodes; } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/node55.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/node55.json index 3107311b792..0a2bf95c5bd 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/node55.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/node55.json @@ -1,14 +1,14 @@ { "url": "http://localhost:8080/nodes/v2/node/host55.yahoo.com", "id": "host55.yahoo.com", - "state": "dirty", + "state": "parked", "type": "tenant", "hostname": "host55.yahoo.com", "openStackId": "node55", "flavor": "[vcpu: 2.0, memory: 8.0 Gb, disk 50.0 Gb, bandwidth: 1.0 Gbps, storage type: local]", "resources":{"vcpu":2.0,"memoryGb":8.0,"diskGb":50.0,"bandwidthGbps":1.0,"diskSpeed":"fast","storageType":"local"}, "environment": "DOCKER_CONTAINER", - "rebootGeneration": 1, + "rebootGeneration": 0, "currentRebootGeneration": 0, "failCount": 0, "wantToRetire": true, @@ -20,7 +20,7 @@ "agent": "system" }, { - "event": "deallocated", + "event": "parked", "at": 123, "agent": "system" } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive-include-deprovisioned.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive-include-deprovisioned.json index 2b650bad39b..03df6c8e1dc 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive-include-deprovisioned.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive-include-deprovisioned.json @@ -17,8 +17,8 @@ @include(node2.json), @include(docker-node1.json), @include(node1.json), - @include(node55.json), @include(node5.json), + @include(node55.json), @include(dockerhost6.json) ] } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive.json index 55e216f454a..8835945dc92 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes-recursive.json @@ -17,7 +17,7 @@ @include(node2.json), @include(docker-node1.json), @include(node1.json), - @include(node55.json), - @include(node5.json) + @include(node5.json), + @include(node55.json) ] } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes.json index 54ff2bc232f..db4d0bb1682 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/nodes.json @@ -52,10 +52,10 @@ "url": "http://localhost:8080/nodes/v2/node/host1.yahoo.com" }, { - "url": "http://localhost:8080/nodes/v2/node/host55.yahoo.com" + "url": "http://localhost:8080/nodes/v2/node/host5.yahoo.com" }, { - "url": "http://localhost:8080/nodes/v2/node/host5.yahoo.com" + "url": "http://localhost:8080/nodes/v2/node/host55.yahoo.com" } ] } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/states-recursive.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/states-recursive.json index 27767be6315..68ff9fedc00 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/states-recursive.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/responses/states-recursive.json @@ -45,7 +45,6 @@ "dirty": { "url": "http://localhost:8080/nodes/v2/state/dirty", "nodes": [ - @include(node55.json) ] }, "failed": { @@ -57,6 +56,7 @@ "parked": { "url": "http://localhost:8080/nodes/v2/state/parked", "nodes": [ + @include(node55.json) ] }, "deprovisioned": { diff --git a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java index e7f74161a8e..65744815ecf 100644 --- a/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java +++ b/vespaclient-container-plugin/src/main/java/com/yahoo/document/restapi/resource/DocumentV1ApiHandler.java @@ -55,6 +55,7 @@ import com.yahoo.jdisc.handler.ResponseHandler; import com.yahoo.jdisc.handler.UnsafeContentInputStream; import com.yahoo.jdisc.http.HttpRequest; import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.messagebus.DynamicThrottlePolicy; import com.yahoo.messagebus.Message; import com.yahoo.messagebus.StaticThrottlePolicy; import com.yahoo.messagebus.Trace; @@ -148,10 +149,12 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private static final String FIELD_SET = "fieldSet"; private static final String SELECTION = "selection"; private static final String CLUSTER = "cluster"; + private static final String DESTINATION_CLUSTER = "destinationCluster"; private static final String CONTINUATION = "continuation"; private static final String WANTED_DOCUMENT_COUNT = "wantedDocumentCount"; private static final String CONCURRENCY = "concurrency"; private static final String BUCKET_SPACE = "bucketSpace"; + private static final String TIME_CHUNK = "timeChunk"; private static final String TIMEOUT = "timeout"; private static final String TRACELEVEL = "tracelevel"; @@ -347,7 +350,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private ContentChannel getDocuments(HttpRequest request, DocumentPath path, ResponseHandler handler) { enqueueAndDispatch(request, handler, () -> { - VisitorParameters parameters = parseParameters(request, path); + VisitorParameters parameters = parseGetParameters(request, path); return () -> { visitAndWrite(request, parameters, handler); return true; // VisitorSession has its own throttle handling. @@ -358,8 +361,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { private ContentChannel postDocuments(HttpRequest request, DocumentPath path, ResponseHandler handler) { enqueueAndDispatch(request, handler, () -> { + StorageCluster destination = resolveCluster(Optional.of(requireProperty(request, DESTINATION_CLUSTER)), clusters); VisitorParameters parameters = parseParameters(request, path); - parameters.setRemoteDataHandler(getProperty(request, ROUTE).orElseThrow(() -> new IllegalArgumentException("Missing required property '" + ROUTE + "'"))); + parameters.setRemoteDataHandler("[Content:cluster=" + destination.name() + "]"); // Bypass indexing. + parameters.setFieldSet(AllFields.NAME); return () -> { visitWithRemote(request, parameters, handler); return true; // VisitorSession has its own throttle handling. @@ -369,19 +374,17 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private ContentChannel putDocuments(HttpRequest request, DocumentPath path, ResponseHandler handler) { - if (getProperty(request, SELECTION).isEmpty()) - throw new IllegalArgumentException("Missing required property '" + SELECTION + "'"); - return new ForwardingContentChannel(in -> { enqueueAndDispatch(request, handler, () -> { - String type = path.documentType().orElseThrow(() -> new IllegalStateException("Document type must be specified for mass updates")); - IdIdString dummyId = new IdIdString("dummy", type, "", ""); + StorageCluster cluster = resolveCluster(Optional.of(requireProperty(request, CLUSTER)), clusters); VisitorParameters parameters = parseParameters(request, path); parameters.setFieldSet(DocIdOnly.NAME); + String type = path.documentType().orElseThrow(() -> new IllegalStateException("Document type must be specified for mass updates")); + IdIdString dummyId = new IdIdString("dummy", type, "", ""); DocumentUpdate update = parser.parseUpdate(in, dummyId.toString()); - update.setCondition(new TestAndSetCondition(parameters.getDocumentSelection())); + update.setCondition(new TestAndSetCondition(requireProperty(request, SELECTION))); return () -> { - visitAndUpdate(request, parameters, handler, update, getProperty(request, ROUTE)); + visitAndUpdate(request, parameters, handler, update, cluster.name()); return true; // VisitorSession has its own throttle handling. }; }); @@ -389,15 +392,13 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private ContentChannel deleteDocuments(HttpRequest request, DocumentPath path, ResponseHandler handler) { - if (getProperty(request, SELECTION).isEmpty()) - throw new IllegalArgumentException("Missing required property '" + SELECTION + "'"); - enqueueAndDispatch(request, handler, () -> { VisitorParameters parameters = parseParameters(request, path); parameters.setFieldSet(DocIdOnly.NAME); - TestAndSetCondition condition = new TestAndSetCondition(parameters.getDocumentSelection()); + TestAndSetCondition condition = new TestAndSetCondition(requireProperty(request, SELECTION)); + StorageCluster cluster = resolveCluster(Optional.of(requireProperty(request, CLUSTER)), clusters); return () -> { - visitAndDelete(request, parameters, handler, condition, getProperty(request, ROUTE)); + visitAndDelete(request, parameters, handler, condition, cluster.name()); return true; // VisitorSession has its own throttle handling. }; }); @@ -478,7 +479,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { .orElse(parameters()); for (String name : names) switch (name) { case CLUSTER: - parameters = getProperty(request, CLUSTER).map(cluster -> resolveCluster(Optional.of(cluster), clusters).route()) + parameters = getProperty(request, CLUSTER).map(cluster -> resolveCluster(Optional.of(cluster), clusters).name()) .map(parameters::withRoute) .orElse(parameters); break; @@ -644,6 +645,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { json.writeStringField("message", message); } + synchronized void writeDocumentCount(long count) throws IOException { + json.writeNumberField("documentCount", count); + } + synchronized void writeDocId(DocumentId id) throws IOException { json.writeStringField("id", id.toString()); } @@ -916,10 +921,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { // ------------------------------------------------- Visits ------------------------------------------------ - private VisitorParameters parseParameters(HttpRequest request, DocumentPath path) { + private VisitorParameters parseGetParameters(HttpRequest request, DocumentPath path) { int wantedDocumentCount = Math.min(1 << 10, getProperty(request, WANTED_DOCUMENT_COUNT, integerParser).orElse(1)); if (wantedDocumentCount <= 0) - throw new IllegalArgumentException("wantedDocumentCount must be positive"); + throw new IllegalArgumentException("wantedDocumentCount must be positive"); int concurrency = Math.min(100, getProperty(request, CONCURRENCY, integerParser).orElse(1)); if (concurrency <= 0) @@ -929,6 +934,25 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { if (cluster.isEmpty() && path.documentType().isEmpty()) throw new IllegalArgumentException("Must set 'cluster' parameter to a valid content cluster id when visiting at a root /document/v1/ level"); + VisitorParameters parameters = parseCommonParameters(request, path, cluster); + parameters.setFieldSet(getProperty(request, FIELD_SET).orElse(path.documentType().map(type -> type + ":[document]").orElse(AllFields.NAME))); + parameters.setMaxTotalHits(wantedDocumentCount); + parameters.setThrottlePolicy(new StaticThrottlePolicy().setMaxPendingCount(concurrency)); + parameters.visitInconsistentBuckets(true); + parameters.setSessionTimeoutMs(Math.max(1, request.getTimeout(TimeUnit.MILLISECONDS) - 5000)); + return parameters; + } + + private VisitorParameters parseParameters(HttpRequest request, DocumentPath path) { + disallow(request, CONCURRENCY, FIELD_SET, ROUTE, WANTED_DOCUMENT_COUNT); + requireProperty(request, SELECTION); + VisitorParameters parameters = parseCommonParameters(request, path, Optional.of(requireProperty(request, CLUSTER))); + parameters.setThrottlePolicy(new DynamicThrottlePolicy().setMinWindowSize(1).setWindowSizeIncrement(1)); + parameters.setSessionTimeoutMs(Math.max(1, getProperty(request, TIME_CHUNK, timeoutMillisParser).orElse(60_000L))); + return parameters; + } + + private VisitorParameters parseCommonParameters(HttpRequest request, DocumentPath path, Optional<String> cluster) { VisitorParameters parameters = new VisitorParameters(Stream.of(getProperty(request, SELECTION), path.documentType(), path.namespace().map(value -> "id.namespace=='" + value + "'"), @@ -940,15 +964,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { .toString()); getProperty(request, CONTINUATION).map(ProgressToken::fromSerializedString).ifPresent(parameters::setResumeToken); - parameters.setFieldSet(getProperty(request, FIELD_SET).orElse(path.documentType().map(type -> type + ":[document]").orElse(AllFields.NAME))); - parameters.setMaxTotalHits(wantedDocumentCount); - parameters.setThrottlePolicy(new StaticThrottlePolicy().setMaxPendingCount(concurrency)); - parameters.setSessionTimeoutMs(Math.max(1, request.getTimeout(TimeUnit.MILLISECONDS) - 5000)); - parameters.visitInconsistentBuckets(true); parameters.setPriority(DocumentProtocol.Priority.NORMAL_4); StorageCluster storageCluster = resolveCluster(cluster, clusters); - parameters.setRoute(storageCluster.route()); + parameters.setRoute(storageCluster.name()); parameters.setBucketSpace(resolveBucket(storageCluster, path.documentType(), List.of(FixedBucketSpaces.defaultSpace(), FixedBucketSpaces.globalSpace()), @@ -969,7 +988,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private void visitAndDelete(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, - TestAndSetCondition condition, Optional<String> route) { + TestAndSetCondition condition, String route) { visitAndProcess(request, parameters, handler, route, (id, operationParameters) -> { DocumentRemove remove = new DocumentRemove(id); remove.setCondition(condition); @@ -978,7 +997,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private void visitAndUpdate(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, - DocumentUpdate protoUpdate, Optional<String> route) { + DocumentUpdate protoUpdate, String route) { visitAndProcess(request, parameters, handler, route, (id, operationParameters) -> { DocumentUpdate update = new DocumentUpdate(protoUpdate); update.setId(id); @@ -987,11 +1006,10 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } private void visitAndProcess(HttpRequest request, VisitorParameters parameters, ResponseHandler handler, - Optional<String> route, BiFunction<DocumentId, DocumentOperationParameters, Result> operation) { + String route, BiFunction<DocumentId, DocumentOperationParameters, Result> operation) { visit(request, parameters, handler, new VisitCallback() { @Override public void onDocument(JsonResponse response, Document document, Runnable ack, Consumer<String> onError) { - DocumentOperationParameters operationParameters = (route.isEmpty() ? parameters() - : parameters().withRoute(route.get())) + DocumentOperationParameters operationParameters = parameters().withRoute(route) .withResponseHandler(operationResponse -> { outstanding.decrementAndGet(); switch (operationResponse.outcome()) { @@ -1057,7 +1075,7 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { callback.onEnd(response); switch (code) { case TIMEOUT: - if ( ! hasVisitedAnyBuckets()) { + if ( ! hasVisitedAnyBuckets() && parameters.getVisitInconsistentBuckets()) { response.writeMessage("No buckets visited within timeout of " + parameters.getSessionTimeoutMs() + "ms (request timeout -5s)"); response.respond(Response.Status.GATEWAY_TIMEOUT); @@ -1066,14 +1084,21 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { case SUCCESS: // Intentional fallthrough. case ABORTED: // Intentional fallthrough. if (error.get() == null) { - if (getProgress() != null && ! getProgress().isFinished()) - response.writeContinuation(getProgress().serializeToString()); + ProgressToken progress = getProgress() != null ? getProgress() : parameters.getResumeToken(); + if (progress != null && ! progress.isFinished()) + response.writeContinuation(progress.serializeToString()); + + if (getVisitorStatistics() != null) + response.writeDocumentCount(getVisitorStatistics().getDocumentsVisited()); response.respond(Response.Status.OK); break; } default: response.writeMessage(error.get() != null ? error.get() : message != null ? message : "Visiting failed"); + if (getVisitorStatistics() != null) + response.writeDocumentCount(getVisitorStatistics().getDocumentsReturned()); + response.respond(Response.Status.INTERNAL_SERVER_ERROR); } visitDispatcher.execute(() -> { @@ -1113,6 +1138,11 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { // ------------------------------------------------ Helpers ------------------------------------------------ + private static String requireProperty(HttpRequest request, String name) { + return getProperty(request, name) + .orElseThrow(() -> new IllegalArgumentException("Must specify '" + name + "' at '" + request.getUri().getRawPath() + "'")); + } + /** Returns the last property with the given name, if present, or throws if this is empty or blank. */ private static Optional<String> getProperty(HttpRequest request, String name) { if ( ! request.parameters().containsKey(name)) @@ -1130,6 +1160,12 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { return getProperty(request, name).map(parser::parse); } + private static void disallow(HttpRequest request, String... properties) { + for (String property : properties) + if (request.parameters().containsKey(property)) + throw new IllegalArgumentException("May not specify '" + property + "' at '" + request.getUri().getRawPath() + "'"); + } + @FunctionalInterface interface Parser<T> extends Function<String, T> { default T parse(String value) { @@ -1177,7 +1213,6 @@ public class DocumentV1ApiHandler extends AbstractRequestHandler { } String name() { return name; } - String route() { return "[Content:cluster=" + name() + "]"; } Optional<String> bucketOf(String documentType) { return Optional.ofNullable(documentBuckets.get(documentType)); } } diff --git a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java index 1e3f3e13c2a..5d81e00cbcb 100644 --- a/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java +++ b/vespaclient-container-plugin/src/test/java/com/yahoo/document/restapi/resource/DocumentV1ApiTest.java @@ -142,8 +142,8 @@ public class DocumentV1ApiTest { public void testResolveCluster() { assertEquals("content", DocumentV1ApiHandler.resolveCluster(Optional.empty(), clusters).name()); - assertEquals("[Content:cluster=content]", - DocumentV1ApiHandler.resolveCluster(Optional.of("content"), clusters).route()); + assertEquals("content", + DocumentV1ApiHandler.resolveCluster(Optional.of("content"), clusters).name()); try { DocumentV1ApiHandler.resolveCluster(Optional.empty(), Map.of()); fail("Should fail without any clusters"); @@ -198,7 +198,7 @@ public class DocumentV1ApiTest { // GET at root is a visit. Numeric parameters have an upper bound. access.expect(tokens); access.expect(parameters -> { - assertEquals("[Content:cluster=content]", parameters.getRoute().toString()); + assertEquals("content", parameters.getRoute().toString()); assertEquals("default", parameters.getBucketSpace()); assertEquals(1024, parameters.getMaxTotalHits()); assertEquals(100, ((StaticThrottlePolicy) parameters.getThrottlePolicy()).getMaxPendingCount()); @@ -211,6 +211,7 @@ public class DocumentV1ApiTest { parameters.getLocalDataHandler().onMessage(new PutDocumentMessage(new DocumentPut(doc3)), tokens.get(2)); VisitorStatistics statistics = new VisitorStatistics(); statistics.setBucketsVisited(1); + statistics.setDocumentsVisited(3); parameters.getControlHandler().onVisitorStatistics(statistics); parameters.getControlHandler().onDone(VisitorControlHandler.CompletionCode.TIMEOUT, "timeout is OK"); }); @@ -235,7 +236,8 @@ public class DocumentV1ApiTest { " \"id\": \"id:space:music:g=a:three\"," + " \"fields\": {}" + " }" + - " ]" + + " ]," + + " \"documentCount\": 3" + "}", response.readAll()); assertEquals(200, response.getStatus()); @@ -252,24 +254,24 @@ public class DocumentV1ApiTest { "}", response.readAll()); assertEquals(400, response.getStatus()); - // POST with namespace and document type is a restricted visit with a required remote data handler ("route") + // POST with namespace and document type is a restricted visit with a required destination cluster ("destinationCluster") access.expect(parameters -> { fail("Not supposed to run"); }); response = driver.sendRequest("http://localhost/document/v1/space/music/docid", POST); assertSameJson("{" + " \"pathId\": \"/document/v1/space/music/docid\"," + - " \"message\": \"Missing required property 'route'\"" + + " \"message\": \"Must specify 'destinationCluster' at '/document/v1/space/music/docid'\"" + "}", response.readAll()); assertEquals(400, response.getStatus()); - // POST with namespace and document type is a restricted visit with a require remote data handler ("route") + // POST with namespace and document type is a restricted visit with a required destination cluster ("destinationCluster") access.expect(parameters -> { - assertEquals("zero", parameters.getRemoteDataHandler()); - assertEquals("music:[document]", parameters.fieldSet()); + assertEquals("[Content:cluster=content]", parameters.getRemoteDataHandler()); + assertEquals("[all]", parameters.fieldSet()); parameters.getControlHandler().onDone(VisitorControlHandler.CompletionCode.SUCCESS, "We made it!"); }); - response = driver.sendRequest("http://localhost/document/v1/space/music/docid?route=zero", POST); + response = driver.sendRequest("http://localhost/document/v1/space/music/docid?destinationCluster=content&selection=true&cluster=content", POST); assertSameJson("{" + " \"pathId\": \"/document/v1/space/music/docid\"" + "}", response.readAll()); @@ -280,19 +282,20 @@ public class DocumentV1ApiTest { access.expect(parameters -> { assertEquals("(true) and (music) and (id.namespace=='space')", parameters.getDocumentSelection()); assertEquals("[id]", parameters.fieldSet()); + assertEquals(10_000, parameters.getSessionTimeoutMs()); parameters.getLocalDataHandler().onMessage(new PutDocumentMessage(new DocumentPut(doc3)), tokens.get(2)); - parameters.getControlHandler().onDone(VisitorControlHandler.CompletionCode.SUCCESS, "Huzzah!"); + parameters.getControlHandler().onDone(VisitorControlHandler.CompletionCode.TIMEOUT, "Won't care"); }); access.session.expect((update, parameters) -> { DocumentUpdate expectedUpdate = new DocumentUpdate(doc3.getDataType(), doc3.getId()); expectedUpdate.addFieldUpdate(FieldUpdate.createAssign(doc3.getField("artist"), new StringFieldValue("Lisa Ekdahl"))); - expectedUpdate.setCondition(new TestAndSetCondition("(true) and (music) and (id.namespace=='space')")); + expectedUpdate.setCondition(new TestAndSetCondition("true")); assertEquals(expectedUpdate, update); parameters.responseHandler().get().handleResponse(new UpdateResponse(0, false)); - assertEquals(parameters().withRoute("zero"), parameters); + assertEquals(parameters().withRoute("content"), parameters); return new Result(Result.ResultType.SUCCESS, null); }); - response = driver.sendRequest("http://localhost/document/v1/space/music/docid?selection=true&route=zero", PUT, + response = driver.sendRequest("http://localhost/document/v1/space/music/docid?selection=true&cluster=content&timeChunk=10", PUT, "{" + " \"fields\": {" + " \"artist\": { \"assign\": \"Lisa Ekdahl\" }" + @@ -303,14 +306,25 @@ public class DocumentV1ApiTest { "}", response.readAll()); assertEquals(200, response.getStatus()); + // PUT with namespace, document type and group is also a restricted visit which requires a cluster. + access.expect(parameters -> { + fail("Not supposed to run"); + }); + response = driver.sendRequest("http://localhost/document/v1/space/music/group/troupe?selection=false", PUT); + assertSameJson("{" + + " \"pathId\": \"/document/v1/space/music/group/troupe\"," + + " \"message\": \"Must specify 'cluster' at '/document/v1/space/music/group/troupe'\"" + + "}", response.readAll()); + assertEquals(400, response.getStatus()); + // PUT with namespace, document type and group is also a restricted visit which requires a selection. access.expect(parameters -> { fail("Not supposed to run"); }); - response = driver.sendRequest("http://localhost/document/v1/space/music/group/troupe", PUT); + response = driver.sendRequest("http://localhost/document/v1/space/music/group/troupe?cluster=content", PUT); assertSameJson("{" + " \"pathId\": \"/document/v1/space/music/group/troupe\"," + - " \"message\": \"Missing required property 'selection'\"" + + " \"message\": \"Must specify 'selection' at '/document/v1/space/music/group/troupe'\"" + "}", response.readAll()); assertEquals(400, response.getStatus()); @@ -319,32 +333,44 @@ public class DocumentV1ApiTest { access.expect(parameters -> { assertEquals("(false) and (music) and (id.namespace=='space')", parameters.getDocumentSelection()); assertEquals("[id]", parameters.fieldSet()); + assertEquals(60_000, parameters.getSessionTimeoutMs()); parameters.getLocalDataHandler().onMessage(new PutDocumentMessage(new DocumentPut(doc2)), tokens.get(0)); parameters.getControlHandler().onDone(VisitorControlHandler.CompletionCode.ABORTED, "Huzzah?"); }); access.session.expect((remove, parameters) -> { DocumentRemove expectedRemove = new DocumentRemove(doc2.getId()); - expectedRemove.setCondition(new TestAndSetCondition("(false) and (music) and (id.namespace=='space')")); - assertEquals(new DocumentRemove(doc2.getId()), remove); - assertEquals(parameters().withRoute("zero"), parameters); + expectedRemove.setCondition(new TestAndSetCondition("false")); + assertEquals(expectedRemove, remove); + assertEquals(parameters().withRoute("content"), parameters); parameters.responseHandler().get().handleResponse(new DocumentIdResponse(0, doc2.getId(), "boom", Response.Outcome.ERROR)); return new Result(Result.ResultType.SUCCESS, null); }); - response = driver.sendRequest("http://localhost/document/v1/space/music/docid?selection=false&route=zero", DELETE); + response = driver.sendRequest("http://localhost/document/v1/space/music/docid?selection=false&cluster=content", DELETE); assertSameJson("{" + " \"pathId\": \"/document/v1/space/music/docid\"," + " \"message\": \"boom\"" + "}", response.readAll()); assertEquals(500, response.getStatus()); - // DELETE at the root is also a deletion visit. These require a selection. + // DELETE at the root is also a deletion visit. These also require a selection. access.expect(parameters -> { fail("Not supposed to run"); }); - response = driver.sendRequest("http://localhost/document/v1/space/music/docid", DELETE); + response = driver.sendRequest("http://localhost/document/v1/", DELETE); assertSameJson("{" + - " \"pathId\": \"/document/v1/space/music/docid\"," + - " \"message\": \"Missing required property 'selection'\"" + + " \"pathId\": \"/document/v1/\"," + + " \"message\": \"Must specify 'selection' at '/document/v1/'\"" + + "}", response.readAll()); + assertEquals(400, response.getStatus()); + + // DELETE at the root is also a deletion visit. These also require a cluster. + access.expect(parameters -> { + fail("Not supposed to run"); + }); + response = driver.sendRequest("http://localhost/document/v1/?selection=true", DELETE); + assertSameJson("{" + + " \"pathId\": \"/document/v1/\"," + + " \"message\": \"Must specify 'cluster' at '/document/v1/'\"" + "}", response.readAll()); assertEquals(400, response.getStatus()); @@ -376,7 +402,7 @@ public class DocumentV1ApiTest { // GET with full document ID is a document get operation which returns 404 when no document is found access.session.expect((id, parameters) -> { assertEquals(doc1.getId(), id); - assertEquals(parameters().withRoute("[Content:cluster=content]").withFieldSet("go"), parameters); + assertEquals(parameters().withRoute("content").withFieldSet("go"), parameters); parameters.responseHandler().get().handleResponse(new DocumentResponse(0, null)); return new Result(Result.ResultType.SUCCESS, null); }); @@ -520,7 +546,7 @@ public class DocumentV1ApiTest { access.session.expect((remove, parameters) -> { DocumentRemove expectedRemove = new DocumentRemove(doc2.getId()); expectedRemove.setCondition(new TestAndSetCondition("false")); - assertEquals(new DocumentRemove(doc2.getId()), remove); + assertEquals(expectedRemove, remove); assertEquals(parameters().withRoute("route"), parameters); parameters.responseHandler().get().handleResponse(new DocumentIdResponse(0, doc2.getId())); return new Result(Result.ResultType.SUCCESS, null); |