Skip to content

Commit 783ebc0

Browse files
authored
ESQL: Reserve memory TopN (#134235) (#134335)
* ESQL: Reserve memory TopN (#134235) Tracks the more memory that's involved in topn. Lucene doesn't track memory usage for TopN and can use a fair bit of it. Try this query: ``` FROM big_table | SORT a, b, c, d, e | LIMIT 1000000 | STATS MAX(a) ``` We attempt to return all million documents from lucene. Is we did this with the compute engine we're track all of the memory usage. With lucene we have to reserve it. In the case of the query above the sort keys weight 8 bytes each. 40 bytes total. Plus another 72 for Lucene's `FieldDoc`. And another 40 at least for copying to the values to `FieldDoc`. That totals something like 152 bytes a piece. That's 145mb. Worth tracking! ## Esql Engine TopN Esql *does* track memory for topn, but it doesn't track the memory used by the min heap itself. It's just a big array of pointers. But it can get very big! * fix backport
1 parent ca96643 commit 783ebc0

File tree

9 files changed

+246
-68
lines changed

9 files changed

+246
-68
lines changed

docs/changelog/134235.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 134235
2+
summary: Reserve memory for Lucene's TopN
3+
area: ES|QL
4+
type: bug
5+
issues: []

test/external-modules/esql-heap-attack/src/javaRestTest/java/org/elasticsearch/xpack/esql/heap_attack/HeapAttackIT.java

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public void skipOnAborted() {
8888
* This used to fail, but we've since compacted top n so it actually succeeds now.
8989
*/
9090
public void testSortByManyLongsSuccess() throws IOException {
91-
initManyLongs();
91+
initManyLongs(10);
9292
Map<String, Object> response = sortByManyLongs(500);
9393
ListMatcher columns = matchesList().item(matchesMap().entry("name", "a").entry("type", "long"))
9494
.item(matchesMap().entry("name", "b").entry("type", "long"));
@@ -105,7 +105,7 @@ public void testSortByManyLongsSuccess() throws IOException {
105105
* This used to crash the node with an out of memory, but now it just trips a circuit breaker.
106106
*/
107107
public void testSortByManyLongsTooMuchMemory() throws IOException {
108-
initManyLongs();
108+
initManyLongs(10);
109109
// 5000 is plenty to break on most nodes
110110
assertCircuitBreaks(attempt -> sortByManyLongs(attempt * 5000));
111111
}
@@ -114,7 +114,7 @@ public void testSortByManyLongsTooMuchMemory() throws IOException {
114114
* This should record an async response with a {@link CircuitBreakingException}.
115115
*/
116116
public void testSortByManyLongsTooMuchMemoryAsync() throws IOException {
117-
initManyLongs();
117+
initManyLongs(10);
118118
Request request = new Request("POST", "/_query/async");
119119
request.addParameter("error_trace", "");
120120
request.setJsonEntity(makeSortByManyLongs(5000).toString().replace("\n", "\\n"));
@@ -191,6 +191,22 @@ public void testSortByManyLongsTooMuchMemoryAsync() throws IOException {
191191
);
192192
}
193193

194+
public void testSortByManyLongsGiantTopN() throws IOException {
195+
initManyLongs(10);
196+
assertMap(
197+
sortBySomeLongsLimit(100000),
198+
matchesMap().entry("took", greaterThan(0))
199+
.entry("is_partial", false)
200+
.entry("columns", List.of(Map.of("name", "MAX(a)", "type", "long")))
201+
.entry("values", List.of(List.of(9)))
202+
);
203+
}
204+
205+
public void testStupidTopN() throws IOException {
206+
initManyLongs(1); // Doesn't actually matter how much data there is.
207+
assertCircuitBreaks(attempt -> sortBySomeLongsLimit(2147483630));
208+
}
209+
194210
private static final int MAX_ATTEMPTS = 5;
195211

196212
interface TryCircuitBreaking {
@@ -249,11 +265,25 @@ private StringBuilder makeSortByManyLongs(int count) {
249265
return query;
250266
}
251267

268+
private Map<String, Object> sortBySomeLongsLimit(int count) throws IOException {
269+
logger.info("sorting by 5 longs, keeping {}", count);
270+
return responseAsMap(query(makeSortBySomeLongsLimit(count), null));
271+
}
272+
273+
private String makeSortBySomeLongsLimit(int count) {
274+
StringBuilder query = new StringBuilder("{\"query\": \"FROM manylongs\n");
275+
query.append("| SORT a, b, c, d, e\n");
276+
query.append("| LIMIT ").append(count).append("\n");
277+
query.append("| STATS MAX(a)\n");
278+
query.append("\"}");
279+
return query.toString();
280+
}
281+
252282
/**
253283
* This groups on about 200 columns which is a lot but has never caused us trouble.
254284
*/
255285
public void testGroupOnSomeLongs() throws IOException {
256-
initManyLongs();
286+
initManyLongs(10);
257287
Response resp = groupOnManyLongs(200);
258288
Map<String, Object> map = responseAsMap(resp);
259289
ListMatcher columns = matchesList().item(matchesMap().entry("name", "MAX(a)").entry("type", "long"));
@@ -265,7 +295,7 @@ public void testGroupOnSomeLongs() throws IOException {
265295
* This groups on 5000 columns which used to throw a {@link StackOverflowError}.
266296
*/
267297
public void testGroupOnManyLongs() throws IOException {
268-
initManyLongs();
298+
initManyLongs(10);
269299
Response resp = groupOnManyLongs(5000);
270300
Map<String, Object> map = responseAsMap(resp);
271301
ListMatcher columns = matchesList().item(matchesMap().entry("name", "MAX(a)").entry("type", "long"));
@@ -333,15 +363,15 @@ private Response concat(int evals) throws IOException {
333363
*/
334364
public void testManyConcat() throws IOException {
335365
int strings = 300;
336-
initManyLongs();
366+
initManyLongs(10);
337367
assertManyStrings(manyConcat("FROM manylongs", strings), strings);
338368
}
339369

340370
/**
341371
* Hits a circuit breaker by building many moderately long strings.
342372
*/
343373
public void testHugeManyConcat() throws IOException {
344-
initManyLongs();
374+
initManyLongs(10);
345375
// 2000 is plenty to break on most nodes
346376
assertCircuitBreaks(attempt -> manyConcat("FROM manylongs", attempt * 2000));
347377
}
@@ -412,15 +442,15 @@ private Map<String, Object> manyConcat(String init, int strings) throws IOExcept
412442
*/
413443
public void testManyRepeat() throws IOException {
414444
int strings = 30;
415-
initManyLongs();
445+
initManyLongs(10);
416446
assertManyStrings(manyRepeat("FROM manylongs", strings), 30);
417447
}
418448

419449
/**
420450
* Hits a circuit breaker by building many moderately long strings.
421451
*/
422452
public void testHugeManyRepeat() throws IOException {
423-
initManyLongs();
453+
initManyLongs(10);
424454
// 75 is plenty to break on most nodes
425455
assertCircuitBreaks(attempt -> manyRepeat("FROM manylongs", attempt * 75));
426456
}
@@ -478,7 +508,7 @@ private void assertManyStrings(Map<String, Object> resp, int strings) throws IOE
478508
}
479509

480510
public void testManyEval() throws IOException {
481-
initManyLongs();
511+
initManyLongs(10);
482512
Map<String, Object> response = manyEval(1);
483513
ListMatcher columns = matchesList();
484514
columns = columns.item(matchesMap().entry("name", "a").entry("type", "long"));
@@ -493,7 +523,7 @@ public void testManyEval() throws IOException {
493523
}
494524

495525
public void testTooManyEval() throws IOException {
496-
initManyLongs();
526+
initManyLongs(10);
497527
// 490 is plenty to fail on most nodes
498528
assertCircuitBreaks(attempt -> manyEval(attempt * 490));
499529
}
@@ -756,24 +786,34 @@ private Map<String, Object> enrichExplosion(int sensorDataCount, int lookupEntri
756786
}
757787
}
758788

759-
private void initManyLongs() throws IOException {
789+
private void initManyLongs(int countPerLong) throws IOException {
760790
logger.info("loading many documents with longs");
761791
StringBuilder bulk = new StringBuilder();
762-
for (int a = 0; a < 10; a++) {
763-
for (int b = 0; b < 10; b++) {
764-
for (int c = 0; c < 10; c++) {
765-
for (int d = 0; d < 10; d++) {
766-
for (int e = 0; e < 10; e++) {
792+
int flush = 0;
793+
for (int a = 0; a < countPerLong; a++) {
794+
for (int b = 0; b < countPerLong; b++) {
795+
for (int c = 0; c < countPerLong; c++) {
796+
for (int d = 0; d < countPerLong; d++) {
797+
for (int e = 0; e < countPerLong; e++) {
767798
bulk.append(String.format(Locale.ROOT, """
768799
{"create":{}}
769800
{"a":%d,"b":%d,"c":%d,"d":%d,"e":%d}
770801
""", a, b, c, d, e));
802+
flush++;
803+
if (flush % 10_000 == 0) {
804+
bulk("manylongs", bulk.toString());
805+
bulk.setLength(0);
806+
logger.info(
807+
"flushing {}/{} to manylongs",
808+
flush,
809+
countPerLong * countPerLong * countPerLong * countPerLong * countPerLong
810+
);
811+
812+
}
771813
}
772814
}
773815
}
774816
}
775-
bulk("manylongs", bulk.toString());
776-
bulk.setLength(0);
777817
}
778818
initIndex("manylongs", bulk.toString());
779819
}

0 commit comments

Comments
 (0)