Skip to content

Commit dabc243

Browse files
leozzxMongoDB Bot
authored and
MongoDB Bot
committed
SERVER-96350 $push, $addToSet, $setUnion, $concatArrays window functions respect memory limits (#35136)
GitOrigin-RevId: 2aeb354c71e4d3aa76d9eb403b936736969703d3
1 parent da46146 commit dabc243

6 files changed

+118
-26
lines changed

jstests/noPassthrough/query/agg/agg_configurable_memory_limits.js

+43-15
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
// @tags: [featureFlagAccuratePercentiles]
2-
3-
import {
4-
testLargeUniformDataset,
5-
testLargeUniformDataset_WithInfinities,
6-
testWithMultipleGroups,
7-
testWithSingleGroup
8-
} from "jstests/aggregation/libs/percentiles_util.js";
9-
101
// Tests that certain aggregation operators have configurable memory limits.
112
const conn = MongoRunner.runMongod();
123
assert.neq(null, conn, "mongod was unable to start up");
134
const db = conn.getDB("test");
145
const coll = db.agg_configurable_memory_limit;
156

7+
// Function to change the parameter value and return the previous value.
8+
function setParam(param, val) {
9+
const res = db.adminCommand({setParameter: 1, [param]: val});
10+
assert.commandWorked(res);
11+
return res.was;
12+
}
13+
1614
// The approximate size of the strings below is 22-25 bytes, so configure one memory limit such that
1715
// 100 of these strings will surely exceed it but 24 of them won't, and another such that 24 will
1816
// exceed as well.
@@ -36,10 +34,23 @@ assert.commandWorked(bulk.execute());
3634
assert.doesNotThrow(() => coll.aggregate([{$group: {_id: null, strings: {$push: "$y"}}}]));
3735

3836
// Now lower the limit to test that its configuration is obeyed.
39-
assert.commandWorked(
40-
db.adminCommand({setParameter: 1, internalQueryMaxPushBytes: memLimitArray}));
37+
const originalVal = setParam('internalQueryMaxPushBytes', memLimitArray);
4138
assert.throwsWithCode(() => coll.aggregate([{$group: {_id: null, strings: {$push: "$y"}}}]),
4239
ErrorCodes.ExceededMemoryLimit);
40+
setParam('internalQueryMaxPushBytes', originalVal);
41+
}());
42+
43+
(function testInternalQueryMaxPushBytesSettingWindowFunc() {
44+
let pipeline = [
45+
{$setWindowFields: {sortBy: {_id: 1}, output: {v: {$push: '$y'}}}},
46+
];
47+
// Test that the default 100MB memory limit isn't reached with our data.
48+
assert.doesNotThrow(() => coll.aggregate(pipeline));
49+
50+
// Now lower the limit to test that its configuration is obeyed.
51+
const originalVal = setParam('internalQueryMaxPushBytes', memLimitArray);
52+
assert.throwsWithCode(() => coll.aggregate(pipeline), ErrorCodes.ExceededMemoryLimit);
53+
setParam('internalQueryMaxPushBytes', originalVal);
4354
}());
4455

4556
(function testInternalQueryMaxAddToSetBytesSetting() {
@@ -48,14 +59,31 @@ assert.commandWorked(bulk.execute());
4859

4960
// Test that $addToSet needs a tighter limit than $push (because some of the strings are the
5061
// same).
51-
assert.commandWorked(
52-
db.adminCommand({setParameter: 1, internalQueryMaxAddToSetBytes: memLimitArray}));
62+
const originalVal = setParam('internalQueryMaxAddToSetBytes', memLimitArray);
5363
assert.doesNotThrow(() => coll.aggregate([{$group: {_id: null, strings: {$addToSet: "$y"}}}]));
5464

55-
assert.commandWorked(
56-
db.adminCommand({setParameter: 1, internalQueryMaxAddToSetBytes: memLimitSet}));
65+
setParam('internalQueryMaxAddToSetBytes', memLimitSet);
5766
assert.throwsWithCode(() => coll.aggregate([{$group: {_id: null, strings: {$addToSet: "$y"}}}]),
5867
ErrorCodes.ExceededMemoryLimit);
68+
setParam('internalQueryMaxAddToSetBytes', originalVal);
69+
}());
70+
71+
(function testInternalQueryMaxAddToSetBytesSettingWindowFunc() {
72+
let pipeline = [
73+
{$setWindowFields: {sortBy: {_id: 1}, output: {v: {$addToSet: '$y'}}}},
74+
];
75+
// Test that the default 100MB memory limit isn't reached with our data.
76+
assert.doesNotThrow(() => coll.aggregate(pipeline));
77+
78+
// Test that $addToSet needs a tighter limit than $concatArrays (because some of the strings are
79+
// the same).
80+
const originalVal = setParam('internalQueryMaxAddToSetBytes', memLimitArray);
81+
assert.doesNotThrow(() => coll.aggregate(pipeline));
82+
83+
// Test that a tighter limit for $addToSet is obeyed.
84+
setParam('internalQueryMaxAddToSetBytes', memLimitSet);
85+
assert.throwsWithCode(() => coll.aggregate(pipeline), ErrorCodes.ExceededMemoryLimit);
86+
setParam('internalQueryMaxAddToSetBytes', originalVal);
5987
}());
6088

6189
(function testInternalQueryTopNAccumulatorBytesSetting() {

jstests/noPassthrough/query/agg/agg_configurable_memory_limits_array_accumulators.js

+47-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Tests that $concatArrays and $setUnion accumulators respect configurable memory limits.
2+
* Tests that $concatArrays and $setUnion accumulators/operators respect configurable memory limits.
33
* @tags: [requires_fcv_81]
44
*/
55

@@ -32,17 +32,40 @@ for (let i = 0; i < nSets; i++) {
3232
}
3333
assert.commandWorked(bulk.execute());
3434

35+
// Function to change the parameter value and return the previous value.
36+
function setParam(param, val) {
37+
const res = db.adminCommand({setParameter: 1, [param]: val});
38+
assert.commandWorked(res);
39+
return res.was;
40+
}
41+
3542
(function testInternalQueryMaxConcatArraysBytesSetting() {
3643
// Test that the default 100MB memory limit isn't reached with our data.
3744
assert.doesNotThrow(
3845
() => coll.aggregate([{$group: {_id: null, strings: {$concatArrays: "$y"}}}]));
3946

4047
// Now lower the limit to test that its configuration is obeyed.
41-
assert.commandWorked(
42-
db.adminCommand({setParameter: 1, internalQueryMaxConcatArraysBytes: memLimitArray}));
48+
const originalVal = setParam('internalQueryMaxConcatArraysBytes', memLimitArray);
4349
assert.throwsWithCode(
4450
() => coll.aggregate([{$group: {_id: null, strings: {$concatArrays: "$y"}}}]),
4551
ErrorCodes.ExceededMemoryLimit);
52+
setParam('internalQueryMaxConcatArraysBytes', originalVal);
53+
}());
54+
55+
(function testInternalQueryMaxConcatArraysBytesSettingWindowFunc() {
56+
let pipeline = [
57+
{
58+
$setWindowFields:
59+
{partitionBy: null, sortBy: {_id: 1}, output: {allStr: {$concatArrays: '$y'}}}
60+
},
61+
];
62+
// Test that the default 100MB memory limit isn't reached with our data.
63+
assert.doesNotThrow(() => coll.aggregate(pipeline));
64+
65+
// Now lower the limit to test that its configuration is obeyed.
66+
const originalVal = setParam('internalQueryMaxConcatArraysBytes', memLimitArray);
67+
assert.throwsWithCode(() => coll.aggregate(pipeline), ErrorCodes.ExceededMemoryLimit);
68+
setParam('internalQueryMaxConcatArraysBytes', originalVal);
4669
}());
4770

4871
(function testInternalQueryMaxSetUnionBytesSetting() {
@@ -51,15 +74,32 @@ assert.commandWorked(bulk.execute());
5174

5275
// Test that $setUnion needs a tighter limit than $concatArrays (because some of the strings are
5376
// the same).
54-
assert.commandWorked(
55-
db.adminCommand({setParameter: 1, internalQueryMaxSetUnionBytes: memLimitArray}));
77+
const originalVal = setParam('internalQueryMaxSetUnionBytes', memLimitArray);
5678
assert.doesNotThrow(() => coll.aggregate([{$group: {_id: null, strings: {$setUnion: "$y"}}}]));
5779

5880
// Test that a tighter limit for $setUnion is obeyed.
59-
assert.commandWorked(
60-
db.adminCommand({setParameter: 1, internalQueryMaxSetUnionBytes: memLimitSet}));
81+
setParam('internalQueryMaxSetUnionBytes', memLimitSet);
6182
assert.throwsWithCode(() => coll.aggregate([{$group: {_id: null, strings: {$setUnion: "$y"}}}]),
6283
ErrorCodes.ExceededMemoryLimit);
84+
setParam('internalQueryMaxSetUnionBytes', originalVal);
85+
}());
86+
87+
(function testInternalQueryMaxSetUnionBytesSettingWindowFunc() {
88+
let pipeline = [
89+
{$setWindowFields: {sortBy: {_id: 1}, output: {v: {$setUnion: '$y'}}}},
90+
];
91+
// Test that the default 100MB memory limit isn't reached with our data.
92+
assert.doesNotThrow(() => coll.aggregate(pipeline));
93+
94+
// Test that $setUnion needs a tighter limit than $concatArrays (because some of the strings are
95+
// the same).
96+
const originalVal = setParam('internalQueryMaxSetUnionBytes', memLimitArray);
97+
assert.doesNotThrow(() => coll.aggregate(pipeline));
98+
99+
// Test that a tighter limit for $setUnion is obeyed.
100+
setParam('internalQueryMaxSetUnionBytes', memLimitSet);
101+
assert.throwsWithCode(() => coll.aggregate(pipeline), ErrorCodes.ExceededMemoryLimit);
102+
setParam('internalQueryMaxSetUnionBytes', originalVal);
63103
}());
64104

65105
MongoRunner.stopMongod(conn);

src/mongo/db/pipeline/window_function/window_function_add_to_set.h

+6-1
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,19 @@ class WindowFunctionAddToSet final : public WindowFunctionState {
5353
}
5454

5555
explicit WindowFunctionAddToSet(ExpressionContext* const expCtx)
56-
: WindowFunctionState(expCtx),
56+
: WindowFunctionState(expCtx, internalQueryMaxAddToSetBytes.load()),
5757
_values(MemoryTokenValueComparator(&_expCtx->getValueComparator())) {
5858
_memUsageTracker.set(sizeof(*this));
5959
}
6060

6161
void add(Value value) override {
6262
_values.emplace(SimpleMemoryUsageToken{value.getApproximateSize(), &_memUsageTracker},
6363
std::move(value));
64+
uassert(ErrorCodes::ExceededMemoryLimit,
65+
str::stream() << "$addToSet used too much memory and cannot spill to disk. Used: "
66+
<< _memUsageTracker.currentMemoryBytes() << " bytes. Memory limit: "
67+
<< _memUsageTracker.maxAllowedMemoryUsageBytes() << " bytes",
68+
_memUsageTracker.withinMemoryLimit());
6469
}
6570

6671
/**

src/mongo/db/pipeline/window_function/window_function_concat_arrays.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class WindowFunctionConcatArrays final : public WindowFunctionState {
4444
}
4545

4646
explicit WindowFunctionConcatArrays(ExpressionContext* const expCtx)
47-
: WindowFunctionState(expCtx) {
47+
: WindowFunctionState(expCtx, internalQueryMaxConcatArraysBytes.load()) {
4848
_memUsageTracker.set(sizeof(*this));
4949
}
5050

@@ -62,6 +62,12 @@ class WindowFunctionConcatArrays final : public WindowFunctionState {
6262
_count += value.getArrayLength();
6363
_values.emplace_back(SimpleMemoryUsageToken{value.getApproximateSize(), &_memUsageTracker},
6464
std::move(value));
65+
uassert(ErrorCodes::ExceededMemoryLimit,
66+
str::stream() << "$concatArrays used too much memory and spilling to disk will not "
67+
"reduce memory usage. Used: "
68+
<< _memUsageTracker.currentMemoryBytes() << " bytes. Memory limit: "
69+
<< _memUsageTracker.maxAllowedMemoryUsageBytes() << " bytes",
70+
_memUsageTracker.withinMemoryLimit());
6571
}
6672

6773
void remove(Value value) override {

src/mongo/db/pipeline/window_function/window_function_push.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ class WindowFunctionPush final : public WindowFunctionState {
5555
return std::make_unique<WindowFunctionPush>(expCtx);
5656
}
5757

58-
explicit WindowFunctionPush(ExpressionContext* const expCtx) : WindowFunctionState(expCtx) {
58+
explicit WindowFunctionPush(ExpressionContext* const expCtx)
59+
: WindowFunctionState(expCtx, internalQueryMaxPushBytes.load()) {
5960
_memUsageTracker.set(sizeof(*this));
6061
}
6162

@@ -65,6 +66,11 @@ class WindowFunctionPush final : public WindowFunctionState {
6566
}
6667
_values.emplace_back(SimpleMemoryUsageToken{value.getApproximateSize(), &_memUsageTracker},
6768
std::move(value));
69+
uassert(ErrorCodes::ExceededMemoryLimit,
70+
str::stream() << "$push used too much memory and cannot spill to disk. Used: "
71+
<< _memUsageTracker.currentMemoryBytes() << "bytes. Memory limit: "
72+
<< _memUsageTracker.maxAllowedMemoryUsageBytes() << " bytes",
73+
_memUsageTracker.withinMemoryLimit());
6874
}
6975

7076
/**

src/mongo/db/pipeline/window_function/window_function_set_union.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class WindowFunctionSetUnion final : public WindowFunctionState {
4343
}
4444

4545
explicit WindowFunctionSetUnion(ExpressionContext* const expCtx)
46-
: WindowFunctionState(expCtx),
46+
: WindowFunctionState(expCtx, internalQueryMaxSetUnionBytes.load()),
4747
_values(MemoryTokenValueComparator(&_expCtx->getValueComparator())) {
4848
_memUsageTracker.set(sizeof(*this));
4949
}
@@ -62,6 +62,13 @@ class WindowFunctionSetUnion final : public WindowFunctionState {
6262
for (const auto& val : value.getArray()) {
6363
_values.emplace(SimpleMemoryUsageToken{val.getApproximateSize(), &_memUsageTracker},
6464
val);
65+
uassert(ErrorCodes::ExceededMemoryLimit,
66+
str::stream() << "$setUnion used too much memory and spilling to disk will not "
67+
"reduce memory usage. Used: "
68+
<< _memUsageTracker.currentMemoryBytes()
69+
<< "bytes. Memory limit: "
70+
<< _memUsageTracker.maxAllowedMemoryUsageBytes() << " bytes",
71+
_memUsageTracker.withinMemoryLimit());
6572
}
6673
}
6774

0 commit comments

Comments
 (0)