diff options
Diffstat (limited to 'container-search/src/test/java/com/yahoo/search/dispatch')
3 files changed, 221 insertions, 17 deletions
diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/InterleavedSearchInvokerTest.java b/container-search/src/test/java/com/yahoo/search/dispatch/InterleavedSearchInvokerTest.java new file mode 100644 index 00000000000..69458f25f93 --- /dev/null +++ b/container-search/src/test/java/com/yahoo/search/dispatch/InterleavedSearchInvokerTest.java @@ -0,0 +1,180 @@ +// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.search.dispatch; + +import com.yahoo.fs4.QueryPacket; +import com.yahoo.prelude.fastsearch.CacheKey; +import com.yahoo.search.Query; +import com.yahoo.search.Result; +import com.yahoo.search.dispatch.searchcluster.Node; +import com.yahoo.search.dispatch.searchcluster.SearchCluster; +import com.yahoo.test.ManualClock; +import org.junit.Test; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; + +import static com.yahoo.search.dispatch.MockSearchCluster.createDispatchConfig; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author ollivir + */ +public class InterleavedSearchInvokerTest { + private ManualClock clock = new ManualClock(Instant.now()); + private Query query = new TestQuery(); + private LinkedList<Event> expectedEvents = new LinkedList<>(); + private List<SearchInvoker> invokers = new ArrayList<>(); + + @Test + public void requireThatAdaptiveTimeoutsAreNotUsedWithFullCoverageRequirement() throws IOException { + SearchCluster cluster = new MockSearchCluster("!", createDispatchConfig(100.0), 1, 3); + SearchInvoker invoker = createInterleavedInvoker(cluster, 3); + + expectedEvents.add(new Event(5000, 100, 0)); + expectedEvents.add(new Event(4900, 100, 1)); + expectedEvents.add(new Event(4800, 100, 2)); + + invoker.search(query, null, null); + + assertTrue("All test scenario events processed", expectedEvents.isEmpty()); + } + + @Test + public void requireThatTimeoutsAreNotMarkedAsAdaptive() throws IOException { + SearchCluster cluster = new MockSearchCluster("!", createDispatchConfig(100.0), 1, 3); + SearchInvoker invoker = createInterleavedInvoker(cluster, 3); + + expectedEvents.add(new Event(5000, 300, 0)); + expectedEvents.add(new Event(4700, 300, 1)); + expectedEvents.add(null); + + List<Result> results = invoker.search(query, null, null); + + assertTrue("All test scenario events processed", expectedEvents.isEmpty()); + assertNotNull("Last invoker is marked as an error", results.get(2).hits().getErrorHit()); + assertTrue("Timed out invoker is a normal timeout", results.get(2).getCoverage(false).isDegradedByTimeout()); + } + + @Test + public void requireThatAdaptiveTimeoutDecreasesTimeoutWhenCoverageIsReached() throws IOException { + SearchCluster cluster = new MockSearchCluster("!", createDispatchConfig(50.0), 1, 4); + SearchInvoker invoker = createInterleavedInvoker(cluster, 4); + + expectedEvents.add(new Event(5000, 100, 0)); + expectedEvents.add(new Event(4900, 100, 1)); + expectedEvents.add(new Event(2400, 100, 2)); + expectedEvents.add(new Event(0, 0, null)); + + List<Result> results = invoker.search(query, null, null); + + assertTrue("All test scenario events processed", expectedEvents.isEmpty()); + assertNotNull("Last invoker is marked as an error", results.get(3).hits().getErrorHit()); + assertTrue("Timed out invoker is an adaptive timeout", results.get(3).getCoverage(false).isDegradedByAdapativeTimeout()); + } + + private InterleavedSearchInvoker createInterleavedInvoker(SearchCluster searchCluster, int numInvokers) { + for (int i = 0; i < numInvokers; i++) { + invokers.add(new TestInvoker()); + } + + return new InterleavedSearchInvoker(invokers, searchCluster) { + @Override + protected long currentTime() { + return clock.millis(); + } + + @Override + protected LinkedBlockingQueue<SearchInvoker> newQueue() { + return new LinkedBlockingQueue<SearchInvoker>() { + @Override + public SearchInvoker poll(long timeout, TimeUnit timeUnit) throws InterruptedException { + assertFalse(expectedEvents.isEmpty()); + Event ev = expectedEvents.removeFirst(); + if (ev == null) { + return null; + } else { + return ev.process(query, timeout); + } + } + }; + } + }; + } + + private class Event { + Long expectedTimeout; + long delay; + Integer invokerIndex; + + public Event(Integer expectedTimeout, int delay, Integer invokerIndex) { + this.expectedTimeout = (long) expectedTimeout; + this.delay = delay; + this.invokerIndex = invokerIndex; + } + + public SearchInvoker process(Query query, long currentTimeout) { + if (expectedTimeout != null) { + assertEquals("Expecting timeout to be " + expectedTimeout, (long) expectedTimeout, currentTimeout); + } + clock.advance(Duration.ofMillis(delay)); + if (query.getTimeLeft() < 0) { + fail("Test sequence ran out of time window"); + } + if (invokerIndex == null) { + return null; + } else { + return invokers.get(invokerIndex); + } + } + } + + private class TestInvoker extends SearchInvoker { + protected TestInvoker() { + super(Optional.of(new Node(42, "?", 0, 0))); + } + + @Override + protected void sendSearchRequest(Query query, QueryPacket queryPacket) throws IOException { + } + + @Override + protected List<Result> getSearchResults(CacheKey cacheKey) throws IOException { + return Collections.singletonList(new Result(query)); + } + + @Override + protected void release() { + } + } + + public class TestQuery extends Query { + private long start = clock.millis(); + + public TestQuery() { + super(); + setTimeout(5000); + } + + @Override + public long getStartTime() { + return start; + } + + @Override + public long getDurationTime() { + return clock.millis() - start; + } + } +} diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java b/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java index 38a753360d8..c056423a9c4 100644 --- a/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java +++ b/container-search/src/test/java/com/yahoo/search/dispatch/LoadBalancerTest.java @@ -7,9 +7,9 @@ import com.yahoo.search.dispatch.searchcluster.SearchCluster; import junit.framework.AssertionFailedError; import org.junit.Test; -import java.util.Arrays; import java.util.Optional; +import static com.yahoo.search.dispatch.MockSearchCluster.createDispatchConfig; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -22,10 +22,10 @@ public class LoadBalancerTest { @Test public void requreThatLoadBalancerServesSingleNodeSetups() { Node n1 = new Node(0, "test-node1", 0, 0); - SearchCluster cluster = new SearchCluster("a", 88.0, 99.0, 0, Arrays.asList(n1), null, 1, null); + SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1), null, 1, null); LoadBalancer lb = new LoadBalancer(cluster, true); - Optional<Group> grp = lb.takeGroupForQuery(null); + Optional<Group> grp = lb.takeGroup(null); Group group = grp.orElseGet(() -> { throw new AssertionFailedError("Expected a SearchCluster.Group"); }); @@ -36,10 +36,10 @@ public class LoadBalancerTest { public void requreThatLoadBalancerServesMultiGroupSetups() { Node n1 = new Node(0, "test-node1", 0, 0); Node n2 = new Node(1, "test-node2", 1, 1); - SearchCluster cluster = new SearchCluster("a", 88.0, 99.0, 0, Arrays.asList(n1, n2), null, 1, null); + SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2), null, 1, null); LoadBalancer lb = new LoadBalancer(cluster, true); - Optional<Group> grp = lb.takeGroupForQuery(null); + Optional<Group> grp = lb.takeGroup(null); Group group = grp.orElseGet(() -> { throw new AssertionFailedError("Expected a SearchCluster.Group"); }); @@ -52,10 +52,10 @@ public class LoadBalancerTest { Node n2 = new Node(1, "test-node2", 1, 0); Node n3 = new Node(0, "test-node3", 0, 1); Node n4 = new Node(1, "test-node4", 1, 1); - SearchCluster cluster = new SearchCluster("a", 88.0, 99.0, 0, Arrays.asList(n1, n2, n3, n4), null, 2, null); + SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2, n3, n4), null, 2, null); LoadBalancer lb = new LoadBalancer(cluster, true); - Optional<Group> grp = lb.takeGroupForQuery(null); + Optional<Group> grp = lb.takeGroup(null); assertThat(grp.isPresent(), is(true)); } @@ -63,18 +63,18 @@ public class LoadBalancerTest { public void requreThatLoadBalancerReturnsDifferentGroups() { Node n1 = new Node(0, "test-node1", 0, 0); Node n2 = new Node(1, "test-node2", 1, 1); - SearchCluster cluster = new SearchCluster("a", 88.0, 99.0, 0, Arrays.asList(n1, n2), null, 1, null); + SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2), null, 1, null); LoadBalancer lb = new LoadBalancer(cluster, true); // get first group - Optional<Group> grp = lb.takeGroupForQuery(null); + Optional<Group> grp = lb.takeGroup(null); Group group = grp.get(); int id1 = group.id(); // release allocation lb.releaseGroup(group); // get second group - grp = lb.takeGroupForQuery(null); + grp = lb.takeGroup(null); group = grp.get(); assertThat(group.id(), not(equalTo(id1))); } @@ -83,16 +83,16 @@ public class LoadBalancerTest { public void requreThatLoadBalancerReturnsGroupWithShortestQueue() { Node n1 = new Node(0, "test-node1", 0, 0); Node n2 = new Node(1, "test-node2", 1, 1); - SearchCluster cluster = new SearchCluster("a", 88.0, 99.0, 0, Arrays.asList(n1, n2), null, 1, null); + SearchCluster cluster = new SearchCluster("a", createDispatchConfig(n1, n2), null, 1, null); LoadBalancer lb = new LoadBalancer(cluster, true); // get first group - Optional<Group> grp = lb.takeGroupForQuery(null); + Optional<Group> grp = lb.takeGroup(null); Group group = grp.get(); int id1 = group.id(); // get second group - grp = lb.takeGroupForQuery(null); + grp = lb.takeGroup(null); group = grp.get(); int id2 = group.id(); assertThat(id2, not(equalTo(id1))); @@ -100,7 +100,7 @@ public class LoadBalancerTest { lb.releaseGroup(group); // get third group - grp = lb.takeGroupForQuery(null); + grp = lb.takeGroup(null); group = grp.get(); assertThat(group.id(), equalTo(id2)); } diff --git a/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java b/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java index fc505097472..f7b92419b52 100644 --- a/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java +++ b/container-search/src/test/java/com/yahoo/search/dispatch/MockSearchCluster.java @@ -6,9 +6,9 @@ import com.google.common.collect.ImmutableMultimap; import com.yahoo.search.dispatch.searchcluster.Group; import com.yahoo.search.dispatch.searchcluster.Node; import com.yahoo.search.dispatch.searchcluster.SearchCluster; +import com.yahoo.vespa.config.search.DispatchConfig; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; @@ -22,7 +22,11 @@ public class MockSearchCluster extends SearchCluster { private final ImmutableMultimap<String, Node> nodesByHost; public MockSearchCluster(String clusterId, int groups, int nodesPerGroup) { - super(clusterId, 100, 100, 0, Collections.emptyList(), null, 1, null); + this(clusterId, createDispatchConfig(), groups, nodesPerGroup); + } + + public MockSearchCluster(String clusterId, DispatchConfig dispatchConfig, int groups, int nodesPerGroup) { + super(clusterId, dispatchConfig, null, 1, null); ImmutableMap.Builder<Integer, Group> groupBuilder = ImmutableMap.builder(); ImmutableMultimap.Builder<String, Node> hostBuilder = ImmutableMultimap.builder(); @@ -58,7 +62,7 @@ public class MockSearchCluster extends SearchCluster { } public Optional<Group> group(int n) { - if(n < numGroups) { + if (n < numGroups) { return Optional.of(groups.get(n)); } else { return Optional.empty(); @@ -80,4 +84,24 @@ public class MockSearchCluster extends SearchCluster { public void failed(Node node) { node.setWorking(false); } + + public static DispatchConfig createDispatchConfig(Node... nodes) { + return createDispatchConfig(100.0, nodes); + } + + public static DispatchConfig createDispatchConfig(double minSearchCoverage, Node... nodes) { + DispatchConfig.Builder builder = new DispatchConfig.Builder(); + builder.minActivedocsPercentage(88.0); + builder.minGroupCoverage(99.0); + builder.maxNodesDownPerGroup(0); + builder.minSearchCoverage(minSearchCoverage); + if(minSearchCoverage < 100.0) { + builder.minWaitAfterCoverageFactor(0); + builder.maxWaitAfterCoverageFactor(0.5); + } + for (Node n : nodes) { + builder.node(new DispatchConfig.Node.Builder().key(n.key()).host(n.hostname()).port(n.fs4port()).group(n.group())); + } + return new DispatchConfig(builder); + } } |