Skip to content

Commit 5bb46e6

Browse files
authored
ESQL: Reserve memory TopN (#134235) (#134331)
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!
1 parent 7b82dfe commit 5bb46e6

File tree

9 files changed

+253
-68
lines changed

9 files changed

+253
-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: 66 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,29 @@ 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+
.entry("documents_found", greaterThan(0))
203+
.entry("values_loaded", greaterThan(0))
204+
);
205+
}
206+
207+
public void testSortByManyLongsGiantTopNTooMuchMemory() throws IOException {
208+
initManyLongs(20);
209+
assertCircuitBreaks(attempt -> sortBySomeLongsLimit(attempt * 500000));
210+
}
211+
212+
public void testStupidTopN() throws IOException {
213+
initManyLongs(1); // Doesn't actually matter how much data there is.
214+
assertCircuitBreaks(attempt -> sortBySomeLongsLimit(2147483630));
215+
}
216+
194217
private static final int MAX_ATTEMPTS = 5;
195218

196219
interface TryCircuitBreaking {
@@ -249,11 +272,25 @@ private StringBuilder makeSortByManyLongs(int count) {
249272
return query;
250273
}
251274

275+
private Map<String, Object> sortBySomeLongsLimit(int count) throws IOException {
276+
logger.info("sorting by 5 longs, keeping {}", count);
277+
return responseAsMap(query(makeSortBySomeLongsLimit(count), null));
278+
}
279+
280+
private String makeSortBySomeLongsLimit(int count) {
281+
StringBuilder query = new StringBuilder("{\"query\": \"FROM manylongs\n");
282+
query.append("| SORT a, b, c, d, e\n");
283+
query.append("| LIMIT ").append(count).append("\n");
284+
query.append("| STATS MAX(a)\n");
285+
query.append("\"}");
286+
return query.toString();
287+
}
288+
252289
/**
253290
* This groups on about 200 columns which is a lot but has never caused us trouble.
254291
*/
255292
public void testGroupOnSomeLongs() throws IOException {
256-
initManyLongs();
293+
initManyLongs(10);
257294
Response resp = groupOnManyLongs(200);
258295
Map<String, Object> map = responseAsMap(resp);
259296
ListMatcher columns = matchesList().item(matchesMap().entry("name", "MAX(a)").entry("type", "long"));
@@ -265,7 +302,7 @@ public void testGroupOnSomeLongs() throws IOException {
265302
* This groups on 5000 columns which used to throw a {@link StackOverflowError}.
266303
*/
267304
public void testGroupOnManyLongs() throws IOException {
268-
initManyLongs();
305+
initManyLongs(10);
269306
Response resp = groupOnManyLongs(5000);
270307
Map<String, Object> map = responseAsMap(resp);
271308
ListMatcher columns = matchesList().item(matchesMap().entry("name", "MAX(a)").entry("type", "long"));
@@ -333,15 +370,15 @@ private Response concat(int evals) throws IOException {
333370
*/
334371
public void testManyConcat() throws IOException {
335372
int strings = 300;
336-
initManyLongs();
373+
initManyLongs(10);
337374
assertManyStrings(manyConcat("FROM manylongs", strings), strings);
338375
}
339376

340377
/**
341378
* Hits a circuit breaker by building many moderately long strings.
342379
*/
343380
public void testHugeManyConcat() throws IOException {
344-
initManyLongs();
381+
initManyLongs(10);
345382
// 2000 is plenty to break on most nodes
346383
assertCircuitBreaks(attempt -> manyConcat("FROM manylongs", attempt * 2000));
347384
}
@@ -412,15 +449,15 @@ private Map<String, Object> manyConcat(String init, int strings) throws IOExcept
412449
*/
413450
public void testManyRepeat() throws IOException {
414451
int strings = 30;
415-
initManyLongs();
452+
initManyLongs(10);
416453
assertManyStrings(manyRepeat("FROM manylongs", strings), 30);
417454
}
418455

419456
/**
420457
* Hits a circuit breaker by building many moderately long strings.
421458
*/
422459
public void testHugeManyRepeat() throws IOException {
423-
initManyLongs();
460+
initManyLongs(10);
424461
// 75 is plenty to break on most nodes
425462
assertCircuitBreaks(attempt -> manyRepeat("FROM manylongs", attempt * 75));
426463
}
@@ -478,7 +515,7 @@ private void assertManyStrings(Map<String, Object> resp, int strings) throws IOE
478515
}
479516

480517
public void testManyEval() throws IOException {
481-
initManyLongs();
518+
initManyLongs(10);
482519
Map<String, Object> response = manyEval(1);
483520
ListMatcher columns = matchesList();
484521
columns = columns.item(matchesMap().entry("name", "a").entry("type", "long"));
@@ -493,7 +530,7 @@ public void testManyEval() throws IOException {
493530
}
494531

495532
public void testTooManyEval() throws IOException {
496-
initManyLongs();
533+
initManyLongs(10);
497534
// 490 is plenty to fail on most nodes
498535
assertCircuitBreaks(attempt -> manyEval(attempt * 490));
499536
}
@@ -808,24 +845,34 @@ private Map<String, Object> enrichExplosion(int sensorDataCount, int lookupEntri
808845
}
809846
}
810847

811-
private void initManyLongs() throws IOException {
848+
private void initManyLongs(int countPerLong) throws IOException {
812849
logger.info("loading many documents with longs");
813850
StringBuilder bulk = new StringBuilder();
814-
for (int a = 0; a < 10; a++) {
815-
for (int b = 0; b < 10; b++) {
816-
for (int c = 0; c < 10; c++) {
817-
for (int d = 0; d < 10; d++) {
818-
for (int e = 0; e < 10; e++) {
851+
int flush = 0;
852+
for (int a = 0; a < countPerLong; a++) {
853+
for (int b = 0; b < countPerLong; b++) {
854+
for (int c = 0; c < countPerLong; c++) {
855+
for (int d = 0; d < countPerLong; d++) {
856+
for (int e = 0; e < countPerLong; e++) {
819857
bulk.append(String.format(Locale.ROOT, """
820858
{"create":{}}
821859
{"a":%d,"b":%d,"c":%d,"d":%d,"e":%d}
822860
""", a, b, c, d, e));
861+
flush++;
862+
if (flush % 10_000 == 0) {
863+
bulk("manylongs", bulk.toString());
864+
bulk.setLength(0);
865+
logger.info(
866+
"flushing {}/{} to manylongs",
867+
flush,
868+
countPerLong * countPerLong * countPerLong * countPerLong * countPerLong
869+
);
870+
871+
}
823872
}
824873
}
825874
}
826875
}
827-
bulk("manylongs", bulk.toString());
828-
bulk.setLength(0);
829876
}
830877
initIndex("manylongs", bulk.toString());
831878
}

0 commit comments

Comments
 (0)