Skip to content
5 changes: 5 additions & 0 deletions docs/changelog/134235.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 134235
summary: Reserve memory for Lucene's TopN
area: ES|QL
type: bug
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public void skipOnAborted() {
* This used to fail, but we've since compacted top n so it actually succeeds now.
*/
public void testSortByManyLongsSuccess() throws IOException {
initManyLongs();
initManyLongs(10);
Map<String, Object> response = sortByManyLongs(500);
ListMatcher columns = matchesList().item(matchesMap().entry("name", "a").entry("type", "long"))
.item(matchesMap().entry("name", "b").entry("type", "long"));
Expand All @@ -108,7 +108,7 @@ public void testSortByManyLongsSuccess() throws IOException {
* This used to crash the node with an out of memory, but now it just trips a circuit breaker.
*/
public void testSortByManyLongsTooMuchMemory() throws IOException {
initManyLongs();
initManyLongs(10);
// 5000 is plenty to break on most nodes
assertCircuitBreaks(attempt -> sortByManyLongs(attempt * 5000));
}
Expand All @@ -117,7 +117,7 @@ public void testSortByManyLongsTooMuchMemory() throws IOException {
* This should record an async response with a {@link CircuitBreakingException}.
*/
public void testSortByManyLongsTooMuchMemoryAsync() throws IOException {
initManyLongs();
initManyLongs(10);
Request request = new Request("POST", "/_query/async");
request.addParameter("error_trace", "");
request.setJsonEntity(makeSortByManyLongs(5000).toString().replace("\n", "\\n"));
Expand Down Expand Up @@ -194,6 +194,29 @@ public void testSortByManyLongsTooMuchMemoryAsync() throws IOException {
);
}

public void testSortByManyLongsGiantTopN() throws IOException {
initManyLongs(10);
assertMap(
sortBySomeLongsLimit(100000),
matchesMap().entry("took", greaterThan(0))
.entry("is_partial", false)
.entry("columns", List.of(Map.of("name", "MAX(a)", "type", "long")))
.entry("values", List.of(List.of(9)))
.entry("documents_found", greaterThan(0))
.entry("values_loaded", greaterThan(0))
);
}

public void testSortByManyLongsGiantTopNTooMuchMemory() throws IOException {
initManyLongs(20);
assertCircuitBreaks(attempt -> sortBySomeLongsLimit(attempt * 500000));
}

public void testStupidTopN() throws IOException {
initManyLongs(1); // Doesn't actually matter how much data there is.
assertCircuitBreaks(attempt -> sortBySomeLongsLimit(2147483630));
}

private static final int MAX_ATTEMPTS = 5;

interface TryCircuitBreaking {
Expand Down Expand Up @@ -252,11 +275,25 @@ private StringBuilder makeSortByManyLongs(int count) {
return query;
}

private Map<String, Object> sortBySomeLongsLimit(int count) throws IOException {
logger.info("sorting by 5 longs, keeping {}", count);
return responseAsMap(query(makeSortBySomeLongsLimit(count), null));
}

private String makeSortBySomeLongsLimit(int count) {
StringBuilder query = new StringBuilder("{\"query\": \"FROM manylongs\n");
query.append("| SORT a, b, c, d, e\n");
query.append("| LIMIT ").append(count).append("\n");
query.append("| STATS MAX(a)\n");
query.append("\"}");
return query.toString();
}

/**
* This groups on about 200 columns which is a lot but has never caused us trouble.
*/
public void testGroupOnSomeLongs() throws IOException {
initManyLongs();
initManyLongs(10);
Response resp = groupOnManyLongs(200);
Map<String, Object> map = responseAsMap(resp);
ListMatcher columns = matchesList().item(matchesMap().entry("name", "MAX(a)").entry("type", "long"));
Expand All @@ -268,7 +305,7 @@ public void testGroupOnSomeLongs() throws IOException {
* This groups on 5000 columns which used to throw a {@link StackOverflowError}.
*/
public void testGroupOnManyLongs() throws IOException {
initManyLongs();
initManyLongs(10);
Response resp = groupOnManyLongs(5000);
Map<String, Object> map = responseAsMap(resp);
ListMatcher columns = matchesList().item(matchesMap().entry("name", "MAX(a)").entry("type", "long"));
Expand Down Expand Up @@ -336,15 +373,15 @@ private Response concat(int evals) throws IOException {
*/
public void testManyConcat() throws IOException {
int strings = 300;
initManyLongs();
initManyLongs(10);
assertManyStrings(manyConcat("FROM manylongs", strings), strings);
}

/**
* Hits a circuit breaker by building many moderately long strings.
*/
public void testHugeManyConcat() throws IOException {
initManyLongs();
initManyLongs(10);
// 2000 is plenty to break on most nodes
assertCircuitBreaks(attempt -> manyConcat("FROM manylongs", attempt * 2000));
}
Expand Down Expand Up @@ -415,15 +452,15 @@ private Map<String, Object> manyConcat(String init, int strings) throws IOExcept
*/
public void testManyRepeat() throws IOException {
int strings = 30;
initManyLongs();
initManyLongs(10);
assertManyStrings(manyRepeat("FROM manylongs", strings), 30);
}

/**
* Hits a circuit breaker by building many moderately long strings.
*/
public void testHugeManyRepeat() throws IOException {
initManyLongs();
initManyLongs(10);
// 75 is plenty to break on most nodes
assertCircuitBreaks(attempt -> manyRepeat("FROM manylongs", attempt * 75));
}
Expand Down Expand Up @@ -481,7 +518,7 @@ private void assertManyStrings(Map<String, Object> resp, int strings) throws IOE
}

public void testManyEval() throws IOException {
initManyLongs();
initManyLongs(10);
Map<String, Object> response = manyEval(1);
ListMatcher columns = matchesList();
columns = columns.item(matchesMap().entry("name", "a").entry("type", "long"));
Expand All @@ -496,7 +533,7 @@ public void testManyEval() throws IOException {
}

public void testTooManyEval() throws IOException {
initManyLongs();
initManyLongs(10);
// 490 is plenty to fail on most nodes
assertCircuitBreaks(attempt -> manyEval(attempt * 490));
}
Expand Down Expand Up @@ -855,24 +892,34 @@ private Map<String, Object> enrichExplosion(int sensorDataCount, int lookupEntri
}
}

private void initManyLongs() throws IOException {
private void initManyLongs(int countPerLong) throws IOException {
logger.info("loading many documents with longs");
StringBuilder bulk = new StringBuilder();
for (int a = 0; a < 10; a++) {
for (int b = 0; b < 10; b++) {
for (int c = 0; c < 10; c++) {
for (int d = 0; d < 10; d++) {
for (int e = 0; e < 10; e++) {
int flush = 0;
for (int a = 0; a < countPerLong; a++) {
for (int b = 0; b < countPerLong; b++) {
for (int c = 0; c < countPerLong; c++) {
for (int d = 0; d < countPerLong; d++) {
for (int e = 0; e < countPerLong; e++) {
bulk.append(String.format(Locale.ROOT, """
{"create":{}}
{"a":%d,"b":%d,"c":%d,"d":%d,"e":%d}
""", a, b, c, d, e));
flush++;
if (flush % 10_000 == 0) {
bulk("manylongs", bulk.toString());
bulk.setLength(0);
logger.info(
"flushing {}/{} to manylongs",
flush,
countPerLong * countPerLong * countPerLong * countPerLong * countPerLong
);

}
}
}
}
}
bulk("manylongs", bulk.toString());
bulk.setLength(0);
}
initIndex("manylongs", bulk.toString());
}
Expand Down
Loading