Skip to content

Commit ebd449e

Browse files
committed
SERVER-17575 find and getMore commands respect BSONObj max user size limit
1 parent 99a9413 commit ebd449e

File tree

7 files changed

+121
-16
lines changed

7 files changed

+121
-16
lines changed

buildscripts/resmokeconfig/suites/sharding_jscore_passthrough.yml

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ selector:
4747
- jstests/core/dbcase2.js # SERVER-11735
4848
# TODO: SERVER-17284 remove once find cmd is implemented in mongos
4949
- jstests/core/read_after_optime.js
50+
- jstests/core/find_getmore_bsonsize.js
5051
- jstests/core/find_getmore_cmd.js
5152

5253
executor:

jstests/core/find_getmore_bsonsize.js

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Ensure that the find and getMore commands can handle documents nearing the 16 MB size limit for
2+
// user-stored BSON documents.
3+
(function() {
4+
'use strict';
5+
6+
var cmdRes;
7+
var collName = 'find_getmore_cmd';
8+
var coll = db[collName];
9+
10+
coll.drop();
11+
12+
var oneKB = 1024;
13+
var oneMB = 1024 * oneKB;
14+
15+
// Build a 1 MB - 1 KB) string.
16+
var smallStr = 'x';
17+
while (smallStr.length < oneMB) {
18+
smallStr += smallStr;
19+
}
20+
assert.eq(smallStr.length, oneMB);
21+
smallStr = smallStr.substring(0, oneMB - oneKB);
22+
23+
// Build a (16 MB - 1 KB) string.
24+
var bigStr = 'y';
25+
while (bigStr.length < (16 * oneMB)) {
26+
bigStr += bigStr;
27+
}
28+
assert.eq(bigStr.length, 16 * oneMB);
29+
bigStr = bigStr.substring(0, (16 * oneMB) - oneKB);
30+
31+
// Collection has one ~1 MB doc followed by one ~16 MB doc.
32+
assert.writeOK(coll.insert({_id: 0, padding: smallStr}));
33+
assert.writeOK(coll.insert({_id: 1, padding: bigStr}));
34+
35+
// Find command should just return the first doc, as adding the last would create an invalid
36+
// command response document.
37+
cmdRes = db.runCommand({find: collName});
38+
assert.commandWorked(cmdRes);
39+
assert.gt(cmdRes.cursor.id, NumberLong(0));
40+
assert.eq(cmdRes.cursor.ns, coll.getFullName());
41+
assert.eq(cmdRes.cursor.firstBatch.length, 1);
42+
43+
// The 16 MB doc should be returned on getMore.
44+
cmdRes = db.runCommand({getMore: cmdRes.cursor.id, collection: collName});
45+
assert.eq(cmdRes.cursor.id, NumberLong(0));
46+
assert.eq(cmdRes.cursor.ns, coll.getFullName());
47+
assert.eq(cmdRes.cursor.nextBatch.length, 1);
48+
49+
// Setup a cursor without returning any results (batchSize of zero).
50+
cmdRes = db.runCommand({find: collName, batchSize: 0});
51+
assert.commandWorked(cmdRes);
52+
assert.gt(cmdRes.cursor.id, NumberLong(0));
53+
assert.eq(cmdRes.cursor.ns, coll.getFullName());
54+
assert.eq(cmdRes.cursor.firstBatch.length, 0);
55+
56+
// First getMore should only return one doc, since both don't fit in the response.
57+
cmdRes = db.runCommand({getMore: cmdRes.cursor.id, collection: collName});
58+
assert.gt(cmdRes.cursor.id, NumberLong(0));
59+
assert.eq(cmdRes.cursor.ns, coll.getFullName());
60+
assert.eq(cmdRes.cursor.nextBatch.length, 1);
61+
62+
// Second getMore should return the second doc and close the cursor.
63+
cmdRes = db.runCommand({getMore: cmdRes.cursor.id, collection: collName});
64+
assert.eq(cmdRes.cursor.id, NumberLong(0));
65+
assert.eq(cmdRes.cursor.ns, coll.getFullName());
66+
assert.eq(cmdRes.cursor.nextBatch.length, 1);
67+
})();

jstests/slow2/sharding_jscore_passthrough.js

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ var db;
8989
'unix_socket\\d*|' +
9090
// TODO: SERVER-17284 remove once find cmd is
9191
// implemented in mongos
92+
'find_getmore_bsonsize|' +
9293
'find_getmore_cmd|' +
9394
'read_after_optime' +
9495
')\.js$');

src/mongo/db/commands/find_cmd.cpp

+7-2
Original file line numberDiff line numberDiff line change
@@ -295,14 +295,19 @@ namespace mongo {
295295
PlanExecutor* exec = cursor->getExecutor();
296296

297297
// 5) Stream query results, adding them to a BSONArray as we go.
298-
//
299-
// TODO: Handle result sets larger than 16MB.
300298
BSONArrayBuilder firstBatch;
301299
BSONObj obj;
302300
PlanExecutor::ExecState state;
303301
int numResults = 0;
304302
while (!enoughForFirstBatch(pq, numResults, firstBatch.len())
305303
&& PlanExecutor::ADVANCED == (state = exec->getNext(&obj, NULL))) {
304+
// If adding this object will cause us to exceed the BSON size limit, then we stash
305+
// it for later.
306+
if (firstBatch.len() + obj.objsize() > BSONObjMaxUserSize && numResults > 0) {
307+
exec->enqueue(obj);
308+
break;
309+
}
310+
306311
// Add result to output buffer.
307312
firstBatch.append(obj);
308313
numResults++;

src/mongo/db/commands/getmore_cmd.cpp

+9-9
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
namespace mongo {
5555

5656
/**
57-
* A command for running getMore() against an existing cursor registered with a
58-
* CursorManager.
57+
* A command for running getMore() against an existing cursor registered with a CursorManager.
58+
* Used to generate the next batch of results for a ClientCursor.
5959
*
6060
* Can be used in combination with any cursor-generating command (e.g. find, aggregate,
6161
* listIndexes).
@@ -101,11 +101,6 @@ namespace mongo {
101101
request.cursorid);
102102
}
103103

104-
/**
105-
* Generates the next batch of results for a ClientCursor.
106-
*
107-
* TODO: Do we need to support some equivalent of OP_REPLY responseFlags?
108-
*/
109104
bool run(OperationContext* txn,
110105
const std::string& dbname,
111106
BSONObj& cmdObj,
@@ -339,11 +334,16 @@ namespace mongo {
339334
// If an awaitData getMore is killed during this process due to our max time expiring at
340335
// an interrupt point, we just continue as normal and return rather than reporting a
341336
// timeout to the user.
342-
//
343-
// TODO: Handle result sets larger than 16MB.
344337
BSONObj obj;
345338
try {
346339
while (PlanExecutor::ADVANCED == (*state = exec->getNext(&obj, NULL))) {
340+
// If adding this object will cause us to exceed the BSON size limit, then we
341+
// stash it for later.
342+
if (nextBatch->len() + obj.objsize() > BSONObjMaxUserSize && *numResults > 0) {
343+
exec->enqueue(obj);
344+
break;
345+
}
346+
347347
// Add result to output buffer.
348348
nextBatch->append(obj);
349349
(*numResults)++;

src/mongo/db/query/plan_executor.cpp

+16-5
Original file line numberDiff line numberDiff line change
@@ -312,14 +312,21 @@ namespace mongo {
312312

313313
PlanExecutor::ExecState PlanExecutor::getNextSnapshotted(Snapshotted<BSONObj>* objOut,
314314
RecordId* dlOut) {
315-
if (killed()) {
315+
if (killed()) {
316316
if (NULL != objOut) {
317317
Status status(ErrorCodes::OperationFailed,
318318
str::stream() << "Operation aborted because: " << *_killReason);
319319
*objOut = Snapshotted<BSONObj>(SnapshotId(),
320320
WorkingSetCommon::buildMemberStatusObject(status));
321321
}
322-
return PlanExecutor::DEAD;
322+
return PlanExecutor::DEAD;
323+
}
324+
325+
if (!_stash.empty()) {
326+
invariant(objOut && !dlOut);
327+
*objOut = {SnapshotId(), _stash.front()};
328+
_stash.pop();
329+
return PlanExecutor::ADVANCED;
323330
}
324331

325332
// When a stage requests a yield for document fetch, it gives us back a RecordFetcher*
@@ -342,8 +349,8 @@ namespace mongo {
342349

343350
if (killed()) {
344351
if (NULL != objOut) {
345-
Status status(ErrorCodes::OperationFailed,
346-
str::stream() << "Operation aborted because: "
352+
Status status(ErrorCodes::OperationFailed,
353+
str::stream() << "Operation aborted because: "
347354
<< *_killReason);
348355
*objOut = Snapshotted<BSONObj>(
349356
SnapshotId(),
@@ -460,7 +467,7 @@ namespace mongo {
460467
}
461468

462469
bool PlanExecutor::isEOF() {
463-
return killed() || _root->isEOF();
470+
return killed() || (_stash.empty() && _root->isEOF());
464471
}
465472

466473
void PlanExecutor::registerExec() {
@@ -537,6 +544,10 @@ namespace mongo {
537544
}
538545
}
539546

547+
void PlanExecutor::enqueue(const BSONObj& obj) {
548+
_stash.push(obj.getOwned());
549+
}
550+
540551
//
541552
// ScopedExecutorRegistration
542553
//

src/mongo/db/query/plan_executor.h

+20
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
#include <boost/optional.hpp>
3232
#include <boost/scoped_ptr.hpp>
33+
#include <queue>
3334

3435
#include "mongo/base/status.h"
3536
#include "mongo/db/invalidation_type.h"
@@ -337,6 +338,20 @@ namespace mongo {
337338
*/
338339
void setYieldPolicy(YieldPolicy policy, bool registerExecutor = true);
339340

341+
/**
342+
* Stash the BSONObj so that it gets returned from the PlanExecutor on a later call to
343+
* getNext().
344+
*
345+
* Enqueued documents are returned in FIFO order. The queued results are exhausted before
346+
* generating further results from the underlying query plan.
347+
*
348+
* Subsequent calls to getNext() must request the BSONObj and *not* the RecordId.
349+
*
350+
* If used in combination with getNextSnapshotted(), then the SnapshotId associated with
351+
* 'obj' will be null when 'obj' is dequeued.
352+
*/
353+
void enqueue(const BSONObj& obj);
354+
340355
private:
341356
/**
342357
* RAII approach to ensuring that plan executors are deregistered.
@@ -422,6 +437,11 @@ namespace mongo {
422437
// TODO make this a non-pointer member. This requires some header shuffling so that this
423438
// file includes plan_yield_policy.h rather than the other way around.
424439
const boost::scoped_ptr<PlanYieldPolicy> _yieldPolicy;
440+
441+
// A stash of results generated by this plan that the user of the PlanExecutor didn't want
442+
// to consume yet. We empty the queue before retrieving further results from the plan
443+
// stages.
444+
std::queue<BSONObj> _stash;
425445
};
426446

427447
} // namespace mongo

0 commit comments

Comments
 (0)