|
78 | 78 | import org.elasticsearch.xcontent.XContentBuilder; |
79 | 79 |
|
80 | 80 | import java.io.IOException; |
| 81 | +import java.io.UncheckedIOException; |
| 82 | +import java.util.ArrayList; |
| 83 | +import java.util.Collection; |
| 84 | +import java.util.List; |
81 | 85 | import java.util.UUID; |
| 86 | +import java.util.concurrent.Callable; |
| 87 | +import java.util.concurrent.ExecutionException; |
| 88 | +import java.util.concurrent.Executor; |
82 | 89 | import java.util.concurrent.ExecutorService; |
83 | 90 | import java.util.concurrent.Executors; |
| 91 | +import java.util.concurrent.Future; |
| 92 | +import java.util.concurrent.FutureTask; |
| 93 | +import java.util.concurrent.LinkedBlockingQueue; |
| 94 | +import java.util.concurrent.RunnableFuture; |
84 | 95 | import java.util.concurrent.ThreadPoolExecutor; |
| 96 | +import java.util.concurrent.TimeUnit; |
| 97 | +import java.util.concurrent.atomic.AtomicInteger; |
85 | 98 | import java.util.function.Function; |
86 | 99 | import java.util.function.Supplier; |
87 | 100 | import java.util.function.ToLongFunction; |
88 | 101 |
|
89 | 102 | import static org.hamcrest.Matchers.equalTo; |
| 103 | +import static org.hamcrest.Matchers.greaterThan; |
90 | 104 | import static org.hamcrest.Matchers.instanceOf; |
91 | 105 | import static org.hamcrest.Matchers.is; |
| 106 | +import static org.hamcrest.Matchers.lessThanOrEqualTo; |
92 | 107 | import static org.mockito.ArgumentMatchers.any; |
93 | 108 | import static org.mockito.ArgumentMatchers.anyString; |
94 | 109 | import static org.mockito.ArgumentMatchers.eq; |
@@ -959,11 +974,92 @@ public void testGetFieldCardinalityRuntimeField() { |
959 | 974 | assertEquals(-1, DefaultSearchContext.getFieldCardinality("field", indexService, null)); |
960 | 975 | } |
961 | 976 |
|
| 977 | + public void testDoNotCreateMoreTasksThanAvailableThreads() throws IOException, ExecutionException, InterruptedException { |
| 978 | + int executorPoolSize = randomIntBetween(2, 5); |
| 979 | + int numIters = randomIntBetween(10, 50); |
| 980 | + int numSegmentTasks = randomIntBetween(50, 100); |
| 981 | + AtomicInteger completedTasks = new AtomicInteger(0); |
| 982 | + ThreadPoolExecutor executor = new ThreadPoolExecutor( |
| 983 | + executorPoolSize, |
| 984 | + executorPoolSize, |
| 985 | + 0L, |
| 986 | + TimeUnit.MILLISECONDS, |
| 987 | + new LinkedBlockingQueue<>() |
| 988 | + ); |
| 989 | + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); |
| 990 | + DefaultSearchContext[] contexts = new DefaultSearchContext[numIters]; |
| 991 | + List<Future<?>> futures = new ArrayList<>(numIters); |
| 992 | + for (int i = 0; i < numIters; i++) { |
| 993 | + contexts[i] = createDefaultSearchContext(executor, randomFrom(SearchService.ResultsType.DFS, SearchService.ResultsType.QUERY)); |
| 994 | + } |
| 995 | + try { |
| 996 | + for (int i = 0; i < numIters; i++) { |
| 997 | + // simulate multiple concurrent search operations that parallelize each their execution across many segment level tasks |
| 998 | + // via Lucene's TaskExecutor. Segment level tasks are never rejected (they execute on the caller upon rejection), but |
| 999 | + // the top-level execute call is subject to rejection once the queue is filled with segment level tasks. That is why |
| 1000 | + // we want to limit the number of tasks that each search can parallelize to |
| 1001 | + // NOTE: DefaultSearchContext does not provide the executor to the searcher once it sees maxPoolSize items in the queue. |
| 1002 | + DefaultSearchContext searchContext = contexts[i]; |
| 1003 | + AtomicInteger segmentTasksCompleted = new AtomicInteger(0); |
| 1004 | + RunnableFuture<Void> task = new FutureTask<>(() -> { |
| 1005 | + Collection<Callable<Void>> tasks = new ArrayList<>(); |
| 1006 | + for (int j = 0; j < numSegmentTasks; j++) { |
| 1007 | + tasks.add(() -> { |
| 1008 | + segmentTasksCompleted.incrementAndGet(); |
| 1009 | + completedTasks.incrementAndGet(); |
| 1010 | + return null; |
| 1011 | + }); |
| 1012 | + } |
| 1013 | + try { |
| 1014 | + searchContext.searcher().getTaskExecutor().invokeAll(tasks); |
| 1015 | + // invokeAll is blocking, hence at this point we are done executing all the sub-tasks, but the queue may |
| 1016 | + // still be filled up with no-op leftover tasks |
| 1017 | + assertEquals(numSegmentTasks, segmentTasksCompleted.get()); |
| 1018 | + } catch (IOException e) { |
| 1019 | + throw new UncheckedIOException(e); |
| 1020 | + } finally { |
| 1021 | + completedTasks.incrementAndGet(); |
| 1022 | + } |
| 1023 | + return null; |
| 1024 | + }); |
| 1025 | + futures.add(task); |
| 1026 | + executor.execute(task); |
| 1027 | + } |
| 1028 | + for (Future<?> future : futures) { |
| 1029 | + future.get(); |
| 1030 | + } |
| 1031 | + assertEquals((long) numIters * numSegmentTasks + numIters, completedTasks.get()); |
| 1032 | + } finally { |
| 1033 | + terminate(executor); |
| 1034 | + for (DefaultSearchContext searchContext : contexts) { |
| 1035 | + searchContext.indexShard().getThreadPool().shutdown(); |
| 1036 | + searchContext.close(); |
| 1037 | + } |
| 1038 | + } |
| 1039 | + // make sure that we do parallelize execution across segments |
| 1040 | + assertThat(executor.getCompletedTaskCount(), greaterThan((long) numIters)); |
| 1041 | + // while not creating too many segment level tasks |
| 1042 | + assertThat(executor.getCompletedTaskCount(), lessThanOrEqualTo((long) numIters * executorPoolSize + numIters)); |
| 1043 | + } |
| 1044 | + |
| 1045 | + private DefaultSearchContext createDefaultSearchContext(Executor executor, SearchService.ResultsType resultsType) throws IOException { |
| 1046 | + return createDefaultSearchContext(Settings.EMPTY, null, executor, resultsType); |
| 1047 | + } |
| 1048 | + |
962 | 1049 | private DefaultSearchContext createDefaultSearchContext(Settings providedIndexSettings) throws IOException { |
963 | 1050 | return createDefaultSearchContext(providedIndexSettings, null); |
964 | 1051 | } |
965 | 1052 |
|
966 | 1053 | private DefaultSearchContext createDefaultSearchContext(Settings providedIndexSettings, XContentBuilder mappings) throws IOException { |
| 1054 | + return createDefaultSearchContext(providedIndexSettings, mappings, null, randomFrom(SearchService.ResultsType.values())); |
| 1055 | + } |
| 1056 | + |
| 1057 | + private DefaultSearchContext createDefaultSearchContext( |
| 1058 | + Settings providedIndexSettings, |
| 1059 | + XContentBuilder mappings, |
| 1060 | + Executor executor, |
| 1061 | + SearchService.ResultsType resultsType |
| 1062 | + ) throws IOException { |
967 | 1063 | TimeValue timeout = new TimeValue(randomIntBetween(1, 100)); |
968 | 1064 | ShardSearchRequest shardSearchRequest = mock(ShardSearchRequest.class); |
969 | 1065 | when(shardSearchRequest.searchType()).thenReturn(SearchType.DEFAULT); |
@@ -1047,9 +1143,9 @@ protected Engine.Searcher acquireSearcherInternal(String source) { |
1047 | 1143 | timeout, |
1048 | 1144 | null, |
1049 | 1145 | false, |
1050 | | - null, |
1051 | | - randomFrom(SearchService.ResultsType.values()), |
1052 | | - randomBoolean(), |
| 1146 | + executor, |
| 1147 | + resultsType, |
| 1148 | + executor != null || randomBoolean(), |
1053 | 1149 | randomInt() |
1054 | 1150 | ); |
1055 | 1151 | } |
|
0 commit comments