Skip to content

Commit fde3877

Browse files
Release aggs earlier
1 parent a2b0d96 commit fde3877

File tree

6 files changed

+115
-96
lines changed

6 files changed

+115
-96
lines changed

server/src/main/java/org/elasticsearch/action/search/QueryPhaseResultConsumer.java

+66-31
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
import org.elasticsearch.action.search.SearchPhaseController.TopDocsStats;
1616
import org.elasticsearch.common.breaker.CircuitBreaker;
1717
import org.elasticsearch.common.breaker.CircuitBreakingException;
18+
import org.elasticsearch.common.collect.Iterators;
1819
import org.elasticsearch.common.io.stream.DelayableWriteable;
1920
import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore;
2021
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
22+
import org.elasticsearch.core.Releasable;
23+
import org.elasticsearch.core.Releasables;
2124
import org.elasticsearch.search.SearchPhaseResult;
2225
import org.elasticsearch.search.SearchService;
2326
import org.elasticsearch.search.SearchShardTarget;
@@ -31,6 +34,7 @@
3134
import java.util.ArrayList;
3235
import java.util.Collections;
3336
import java.util.Comparator;
37+
import java.util.Iterator;
3438
import java.util.List;
3539
import java.util.concurrent.Executor;
3640
import java.util.concurrent.atomic.AtomicReference;
@@ -174,14 +178,10 @@ public SearchPhaseController.ReducedQueryPhase reduce() throws Exception {
174178
this.mergeResult = null;
175179
final int resultSize = buffer.size() + (mergeResult == null ? 0 : 1);
176180
final List<TopDocs> topDocsList = hasTopDocs ? new ArrayList<>(resultSize) : null;
177-
final List<DelayableWriteable<InternalAggregations>> aggsList = hasAggs ? new ArrayList<>(resultSize) : null;
178181
if (mergeResult != null) {
179182
if (topDocsList != null) {
180183
topDocsList.add(mergeResult.reducedTopDocs);
181184
}
182-
if (aggsList != null) {
183-
aggsList.add(DelayableWriteable.referencing(mergeResult.reducedAggs));
184-
}
185185
}
186186
for (QuerySearchResult result : buffer) {
187187
topDocsStats.add(result.topDocs(), result.searchTimedOut(), result.terminatedEarly());
@@ -190,34 +190,39 @@ public SearchPhaseController.ReducedQueryPhase reduce() throws Exception {
190190
setShardIndex(topDocs.topDocs, result.getShardIndex());
191191
topDocsList.add(topDocs.topDocs);
192192
}
193-
if (aggsList != null) {
194-
aggsList.add(result.getAggs());
195-
}
196193
}
197194
SearchPhaseController.ReducedQueryPhase reducePhase;
198195
long breakerSize = circuitBreakerBytes;
196+
final InternalAggregations aggs;
199197
try {
200-
if (aggsList != null) {
198+
if (hasAggs) {
201199
// Add an estimate of the final reduce size
202200
breakerSize = addEstimateAndMaybeBreak(estimateRamBytesUsedForReduce(breakerSize));
201+
aggs = aggregate(
202+
buffer.iterator(),
203+
mergeResult,
204+
resultSize,
205+
performFinalReduce ? aggReduceContextBuilder.forFinalReduction() : aggReduceContextBuilder.forPartialReduction()
206+
);
207+
} else {
208+
aggs = null;
203209
}
204210
reducePhase = SearchPhaseController.reducedQueryPhase(
205211
results.asList(),
206-
aggsList,
212+
aggs,
207213
topDocsList == null ? Collections.emptyList() : topDocsList,
208214
topDocsStats,
209215
numReducePhases,
210216
false,
211-
aggReduceContextBuilder,
212-
queryPhaseRankCoordinatorContext,
213-
performFinalReduce
217+
queryPhaseRankCoordinatorContext
214218
);
219+
buffer = null;
215220
} finally {
216221
releaseAggs(buffer);
217222
}
218223
if (hasAggs
219224
// reduced aggregations can be null if all shards failed
220-
&& reducePhase.aggregations() != null) {
225+
&& aggs != null) {
221226

222227
// Update the circuit breaker to replace the estimation with the serialized size of the newly reduced result
223228
long finalSize = DelayableWriteable.getSerializedSize(reducePhase.aggregations()) - breakerSize;
@@ -249,17 +254,7 @@ private MergeResult partialReduce(
249254
toConsume.sort(RESULT_COMPARATOR);
250255

251256
final TopDocs newTopDocs;
252-
final InternalAggregations newAggs;
253-
final List<DelayableWriteable<InternalAggregations>> aggsList;
254257
final int resultSetSize = toConsume.size() + (lastMerge != null ? 1 : 0);
255-
if (hasAggs) {
256-
aggsList = new ArrayList<>(resultSetSize);
257-
if (lastMerge != null) {
258-
aggsList.add(DelayableWriteable.referencing(lastMerge.reducedAggs));
259-
}
260-
} else {
261-
aggsList = null;
262-
}
263258
List<TopDocs> topDocsList;
264259
if (hasTopDocs) {
265260
topDocsList = new ArrayList<>(resultSetSize);
@@ -269,14 +264,12 @@ private MergeResult partialReduce(
269264
} else {
270265
topDocsList = null;
271266
}
267+
final InternalAggregations newAggs;
272268
try {
273269
for (QuerySearchResult result : toConsume) {
274270
topDocsStats.add(result.topDocs(), result.searchTimedOut(), result.terminatedEarly());
275271
SearchShardTarget target = result.getSearchShardTarget();
276272
processedShards.add(new SearchShard(target.getClusterAlias(), target.getShardId()));
277-
if (aggsList != null) {
278-
aggsList.add(result.getAggs());
279-
}
280273
if (topDocsList != null) {
281274
TopDocsAndMaxScore topDocs = result.consumeTopDocs();
282275
setShardIndex(topDocs.topDocs, result.getShardIndex());
@@ -285,9 +278,10 @@ private MergeResult partialReduce(
285278
}
286279
// we have to merge here in the same way we collect on a shard
287280
newTopDocs = topDocsList == null ? null : mergeTopDocs(topDocsList, topNSize, 0);
288-
newAggs = aggsList == null
289-
? null
290-
: InternalAggregations.topLevelReduceDelayable(aggsList, aggReduceContextBuilder.forPartialReduction());
281+
newAggs = hasAggs
282+
? aggregate(toConsume.iterator(), lastMerge, resultSetSize, aggReduceContextBuilder.forPartialReduction())
283+
: null;
284+
toConsume = null;
291285
} finally {
292286
releaseAggs(toConsume);
293287
}
@@ -302,6 +296,45 @@ private MergeResult partialReduce(
302296
return new MergeResult(processedShards, newTopDocs, newAggs, newAggs != null ? DelayableWriteable.getSerializedSize(newAggs) : 0);
303297
}
304298

299+
private static InternalAggregations aggregate(
300+
Iterator<QuerySearchResult> toConsume,
301+
MergeResult lastMerge,
302+
int resultSetSize,
303+
AggregationReduceContext reduceContext
304+
) {
305+
interface ReleasableIterator extends Iterator<InternalAggregations>, Releasable {}
306+
try (var aggsIter = new ReleasableIterator() {
307+
308+
private Releasable toRelease;
309+
310+
@Override
311+
public void close() {
312+
Releasables.close(toRelease);
313+
}
314+
315+
@Override
316+
public boolean hasNext() {
317+
return toConsume.hasNext();
318+
}
319+
320+
@Override
321+
public InternalAggregations next() {
322+
var res = toConsume.next().consumeAggs();
323+
Releasables.close(toRelease);
324+
toRelease = res;
325+
return res.expand();
326+
}
327+
}) {
328+
return InternalAggregations.topLevelReduce(
329+
lastMerge == null ? aggsIter : Iterators.concat(Iterators.single(lastMerge.reducedAggs), aggsIter),
330+
resultSetSize,
331+
reduceContext
332+
);
333+
} finally {
334+
toConsume.forEachRemaining(QuerySearchResult::releaseAggs);
335+
}
336+
}
337+
305338
public int getNumReducePhases() {
306339
return numReducePhases;
307340
}
@@ -517,8 +550,10 @@ public void onFailure(Exception exc) {
517550
}
518551

519552
private static void releaseAggs(List<QuerySearchResult> toConsume) {
520-
for (QuerySearchResult result : toConsume) {
521-
result.releaseAggs();
553+
if (toConsume != null) {
554+
for (QuerySearchResult result : toConsume) {
555+
result.releaseAggs();
556+
}
522557
}
523558
}
524559

server/src/main/java/org/elasticsearch/action/search/SearchPhaseController.java

+4-12
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import org.apache.lucene.search.TotalHits;
2121
import org.apache.lucene.search.TotalHits.Relation;
2222
import org.elasticsearch.common.breaker.CircuitBreaker;
23-
import org.elasticsearch.common.io.stream.DelayableWriteable;
2423
import org.elasticsearch.common.lucene.Lucene;
2524
import org.elasticsearch.common.lucene.search.TopDocsAndMaxScore;
2625
import org.elasticsearch.common.util.Maps;
@@ -401,22 +400,20 @@ private static SearchHits getHits(
401400
/**
402401
* Reduces the given query results and consumes all aggregations and profile results.
403402
* @param queryResults a list of non-null query shard results
404-
* @param bufferedAggs a list of pre-collected aggregations.
403+
* @param reducedAggs already reduced aggregations
405404
* @param bufferedTopDocs a list of pre-collected top docs.
406405
* @param numReducePhases the number of non-final reduce phases applied to the query results.
407406
* @see QuerySearchResult#getAggs()
408407
* @see QuerySearchResult#consumeProfileResult()
409408
*/
410409
static ReducedQueryPhase reducedQueryPhase(
411410
Collection<? extends SearchPhaseResult> queryResults,
412-
@Nullable List<DelayableWriteable<InternalAggregations>> bufferedAggs,
411+
@Nullable InternalAggregations reducedAggs,
413412
List<TopDocs> bufferedTopDocs,
414413
TopDocsStats topDocsStats,
415414
int numReducePhases,
416415
boolean isScrollRequest,
417-
AggregationReduceContext.Builder aggReduceContextBuilder,
418-
QueryPhaseRankCoordinatorContext queryPhaseRankCoordinatorContext,
419-
boolean performFinalReduce
416+
QueryPhaseRankCoordinatorContext queryPhaseRankCoordinatorContext
420417
) {
421418
assert numReducePhases >= 0 : "num reduce phases must be >= 0 but was: " + numReducePhases;
422419
numReducePhases++; // increment for this phase
@@ -520,12 +517,7 @@ static ReducedQueryPhase reducedQueryPhase(
520517
topDocsStats.timedOut,
521518
topDocsStats.terminatedEarly,
522519
reducedSuggest,
523-
bufferedAggs == null
524-
? null
525-
: InternalAggregations.topLevelReduceDelayable(
526-
bufferedAggs,
527-
performFinalReduce ? aggReduceContextBuilder.forFinalReduction() : aggReduceContextBuilder.forPartialReduction()
528-
),
520+
reducedAggs,
529521
profileShardResults.isEmpty() ? null : new SearchProfileResultsBuilder(profileShardResults),
530522
sortedTopDocs,
531523
sortValueFormats,

server/src/main/java/org/elasticsearch/action/search/SearchScrollAsyncAction.java

+1-11
Original file line numberDiff line numberDiff line change
@@ -339,16 +339,6 @@ public AggregationReduceContext forFinalReduction() {
339339
topDocs.add(td.topDocs);
340340
}
341341
}
342-
return SearchPhaseController.reducedQueryPhase(
343-
queryResults,
344-
null,
345-
topDocs,
346-
topDocsStats,
347-
0,
348-
true,
349-
aggReduceContextBuilder,
350-
null,
351-
true
352-
);
342+
return SearchPhaseController.reducedQueryPhase(queryResults, null, topDocs, topDocsStats, 0, true, null);
353343
}
354344
}

server/src/main/java/org/elasticsearch/search/aggregations/InternalAggregations.java

+31-33
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*/
99
package org.elasticsearch.search.aggregations;
1010

11-
import org.elasticsearch.common.io.stream.DelayableWriteable;
1211
import org.elasticsearch.common.io.stream.StreamInput;
1312
import org.elasticsearch.common.io.stream.StreamOutput;
1413
import org.elasticsearch.common.io.stream.Writeable;
@@ -23,7 +22,6 @@
2322
import org.elasticsearch.xcontent.XContentBuilder;
2423

2524
import java.io.IOException;
26-
import java.util.AbstractList;
2725
import java.util.ArrayList;
2826
import java.util.Iterator;
2927
import java.util.List;
@@ -180,44 +178,22 @@ public SortValue sortValue(AggregationPath.PathElement head, Iterator<Aggregatio
180178
}
181179

182180
/**
183-
* Equivalent to {@link #topLevelReduce(List, AggregationReduceContext)} but it takes a list of
184-
* {@link DelayableWriteable}. The object will be expanded once via {@link DelayableWriteable#expand()}
185-
* but it is the responsibility of the caller to release those releasables.
181+
* Equivalent to {@link #topLevelReduce(List, AggregationReduceContext)} but it takes an iterator and a count.
186182
*/
187-
public static InternalAggregations topLevelReduceDelayable(
188-
List<DelayableWriteable<InternalAggregations>> delayableAggregations,
189-
AggregationReduceContext context
190-
) {
191-
final List<InternalAggregations> aggregations = new AbstractList<>() {
192-
@Override
193-
public InternalAggregations get(int index) {
194-
return delayableAggregations.get(index).expand();
195-
}
196-
197-
@Override
198-
public int size() {
199-
return delayableAggregations.size();
200-
}
201-
};
202-
return topLevelReduce(aggregations, context);
183+
public static InternalAggregations topLevelReduce(Iterator<InternalAggregations> aggs, int count, AggregationReduceContext context) {
184+
if (count == 0) {
185+
return null;
186+
}
187+
return maybeExecuteFinalReduce(context, count == 1 ? reduce(aggs.next(), context) : reduce(aggs, count, context));
203188
}
204189

205-
/**
206-
* Begin the reduction process. This should be the entry point for the "first" reduction, e.g. called by
207-
* SearchPhaseController or anywhere else that wants to initiate a reduction. It _should not_ be called
208-
* as an intermediate reduction step (e.g. in the middle of an aggregation tree).
209-
*
210-
* This method first reduces the aggregations, and if it is the final reduce, then reduce the pipeline
211-
* aggregations (both embedded parent/sibling as well as top-level sibling pipelines)
212-
*/
213-
public static InternalAggregations topLevelReduce(List<InternalAggregations> aggregationsList, AggregationReduceContext context) {
214-
InternalAggregations reduced = reduce(aggregationsList, context);
190+
private static InternalAggregations maybeExecuteFinalReduce(AggregationReduceContext context, InternalAggregations reduced) {
215191
if (reduced == null) {
216192
return null;
217193
}
218194
if (context.isFinalReduce()) {
219-
List<InternalAggregation> reducedInternalAggs = reduced.getInternalAggregations();
220-
reducedInternalAggs = reducedInternalAggs.stream()
195+
List<InternalAggregation> reducedInternalAggs = reduced.getInternalAggregations()
196+
.stream()
221197
.map(agg -> agg.reducePipelines(agg, context, context.pipelineTreeRoot().subTree(agg.getName())))
222198
.collect(Collectors.toCollection(ArrayList::new));
223199

@@ -231,6 +207,18 @@ public static InternalAggregations topLevelReduce(List<InternalAggregations> agg
231207
return reduced;
232208
}
233209

210+
/**
211+
* Begin the reduction process. This should be the entry point for the "first" reduction, e.g. called by
212+
* SearchPhaseController or anywhere else that wants to initiate a reduction. It _should not_ be called
213+
* as an intermediate reduction step (e.g. in the middle of an aggregation tree).
214+
*
215+
* This method first reduces the aggregations, and if it is the final reduce, then reduce the pipeline
216+
* aggregations (both embedded parent/sibling as well as top-level sibling pipelines)
217+
*/
218+
public static InternalAggregations topLevelReduce(List<InternalAggregations> aggregationsList, AggregationReduceContext context) {
219+
return maybeExecuteFinalReduce(context, reduce(aggregationsList, context));
220+
}
221+
234222
/**
235223
* Reduces the given list of aggregations as well as the top-level pipeline aggregators extracted from the first
236224
* {@link InternalAggregations} object found in the list.
@@ -254,6 +242,16 @@ public static InternalAggregations reduce(List<InternalAggregations> aggregation
254242
}
255243
}
256244

245+
private static InternalAggregations reduce(Iterator<InternalAggregations> aggsIterator, int count, AggregationReduceContext context) {
246+
// general case
247+
var first = aggsIterator.next();
248+
try (AggregatorsReducer reducer = new AggregatorsReducer(first, context, count)) {
249+
reducer.accept(first);
250+
aggsIterator.forEachRemaining(reducer::accept);
251+
return reducer.get();
252+
}
253+
}
254+
257255
public static InternalAggregations reduce(InternalAggregations aggregations, AggregationReduceContext context) {
258256
final List<InternalAggregation> internalAggregations = aggregations.asList();
259257
int size = internalAggregations.size();

server/src/main/java/org/elasticsearch/search/query/QuerySearchResult.java

+9
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,15 @@ public DelayableWriteable<InternalAggregations> getAggs() {
236236
return aggregations;
237237
}
238238

239+
public DelayableWriteable<InternalAggregations> consumeAggs() {
240+
if (aggregations == null) {
241+
throw new IllegalStateException("aggs already released");
242+
}
243+
var res = aggregations;
244+
aggregations = null;
245+
return res;
246+
}
247+
239248
/**
240249
* Release the memory hold by the {@link DelayableWriteable} aggregations
241250
* @throws IllegalStateException if {@link #releaseAggs()} has already being called.

0 commit comments

Comments
 (0)