diff options
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error.cpp | 277 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error.h | 10 | ||||
-rw-r--r-- | src/mongo/db/matcher/doc_validation_error_test.cpp | 192 | ||||
-rw-r--r-- | src/mongo/db/query/query_knobs.idl | 11 |
4 files changed, 425 insertions, 65 deletions
diff --git a/src/mongo/db/matcher/doc_validation_error.cpp b/src/mongo/db/matcher/doc_validation_error.cpp index 74047cb2fc0..2e8181cf90d 100644 --- a/src/mongo/db/matcher/doc_validation_error.cpp +++ b/src/mongo/db/matcher/doc_validation_error.cpp @@ -70,6 +70,9 @@ using AnnotationMode = ErrorAnnotation::Mode; using LeafArrayBehavior = ElementPath::LeafArrayBehavior; using NonLeafArrayBehavior = ElementPath::NonLeafArrayBehavior; +// Fail point which simulates an internal error for testing. +MONGO_FAIL_POINT_DEFINE(docValidationInternalErrorFailPoint); + /** * Enumerated type which describes whether an error should be described normally or in an * inverted sense when in a negated context. More precisely, when a MatchExpression fails to match a @@ -115,6 +118,8 @@ struct ValidationErrorFrame { BSONObj currentDoc; // Tracks whether the generated error should be described normally or in an inverted context. InvertError inversion; + // Tracks whether the array of 'consideredValues' was truncated for this frame. + bool consideredValuesTruncated = false; }; using RuntimeState = ValidationErrorFrame::RuntimeState; @@ -123,7 +128,17 @@ using RuntimeState = ValidationErrorFrame::RuntimeState; * A struct which tracks context during error generation. */ struct ValidationErrorContext { - ValidationErrorContext(const BSONObj& rootDoc) : rootDoc(rootDoc) {} + ValidationErrorContext(const BSONObj& rootDoc, + bool truncate, + const int maxDocValidationErrorSize, + const int maxConsideredValuesElements) + : rootDoc(rootDoc), + truncate(truncate), + kMaxDocValidationErrorSize(maxDocValidationErrorSize), + kMaxConsideredValuesElements(maxConsideredValuesElements) { + invariant(kMaxConsideredValuesElements > 0); + invariant(kMaxDocValidationErrorSize > 0); + } /** * Utilities which add/remove ValidationErrorFrames from 'frames'. @@ -228,10 +243,12 @@ struct ValidationErrorContext { * Appends the latest complete error to 'builder'. */ void appendLatestCompleteError(BSONObjBuilder* builder) { + const static std::string kDetailsString = "details"; stdx::visit( - visit_helper::Overloaded{ - [&](const auto& details) -> void { builder->append("details", details); }, - [&](const std::monostate& arr) -> void { MONGO_UNREACHABLE }}, + visit_helper::Overloaded{[&](const auto& details) -> void { + verifySizeAndAppend(details, kDetailsString, builder); + }, + [&](const std::monostate& arr) -> void { MONGO_UNREACHABLE }}, latestCompleteError); } @@ -246,9 +263,17 @@ struct ValidationErrorContext { /** * Returns whether 'expr' will produce an array as an error. */ - bool producesArray(const MatchExpression& expr) { + bool producesArray(const MatchExpression& expr) const { return expr.getErrorAnnotation()->operatorName == "_internalSubschema"; } + bool isConsideredValuesTruncated() const { + invariant(!frames.empty()); + return frames.top().consideredValuesTruncated; + } + void markConsideredValuesAsTruncated() { + invariant(!frames.empty()); + frames.top().consideredValuesTruncated = true; + } /** * Finishes error for 'expr' by stashing its generated error if it made one and popping the @@ -281,6 +306,46 @@ struct ValidationErrorContext { getCurrentRuntimeState() != RuntimeState::kNoError; } + /** + * Verify that the size of 'builder' combined with that of 'item' are of valid size before + * appending the latter to the former; throws a BSONObjectTooLarge error otherwise. + */ + template <class ItemType, class BuilderType> + void verifySize(const ItemType& item, const BuilderType& builder) { + uassert(ErrorCodes::BSONObjectTooLarge, + "doc validation error builder exceeded maximum size", + builder.len() + item.objsize() <= kMaxDocValidationErrorSize); + } + + template <class BuilderType> + void verifySize(const BSONElement& item, const BuilderType& builder) { + uassert(ErrorCodes::BSONObjectTooLarge, + "doc validation error builder exceeded maximum size", + builder.len() + item.size() <= kMaxDocValidationErrorSize); + } + + template <class ItemType, class BuilderType> + void verifySizeAndAppend(const ItemType& item, + const std::string& fieldName, + BuilderType* builder) { + verifySize(item, *builder); + builder->append(fieldName, item); + } + + template <class ItemType> + void verifySizeAndAppend(const ItemType& item, BSONArrayBuilder* builder) { + verifySize(item, *builder); + builder->append(item); + } + + template <class BuilderType> + void verifySizeAndAppendAs(const BSONElement& item, + const std::string& fieldName, + BuilderType* builder) { + verifySize(item, *builder); + builder->appendAs(item, fieldName); + } + // Frames which construct the generated error. Each frame corresponds to the information needed // to generate an error for one node. As such, each node must call 'pushNewFrame' as part of // its pre-visit and 'popFrame' as part of its post-visit. @@ -297,6 +362,14 @@ struct ValidationErrorContext { stdx::variant<std::monostate, BSONObj, BSONArray> latestCompleteError = std::monostate(); // Document which failed to match against the collection's validator. const BSONObj& rootDoc; + // Tracks whether the generated error should omit appending 'specifiedAs' and + // 'consideredValues' to avoid generating an error larger than the maximum BSONObj size. + const bool truncate = false; + // The maximum allowed size for a doc validation error. + const int kMaxDocValidationErrorSize; + // Tracks the maximum number of values that will be reported in the 'consideredValues' array + // for leaf operators. + const int kMaxConsideredValuesElements; }; /** @@ -320,7 +393,8 @@ void finishLogicalOperatorChildError(const ListOfMatchExpression* expr, ctx->appendLatestCompleteError(&subBuilder); subBuilder.done(); } else { - ctx->getCurrentArrayBuilder().append(ctx->getLatestCompleteErrorObject()); + ctx->verifySizeAndAppend(ctx->getLatestCompleteErrorObject(), + &ctx->getCurrentArrayBuilder()); } } } @@ -640,7 +714,8 @@ public: appendConsideredValue(attributeValueAsArray); auto duplicateValue = expr->findFirstDuplicateValue(attributeValueAsArray); invariant(duplicateValue); - _context->getCurrentObjBuilder().appendAs(duplicateValue, "duplicatedValue"_sd); + _context->verifySizeAndAppendAs( + duplicateValue, "duplicatedValue", &_context->getCurrentObjBuilder()); } else { _context->setCurrentRuntimeState(RuntimeState::kNoError); } @@ -763,7 +838,14 @@ private: } } void appendSpecifiedAs(const ErrorAnnotation& annotation, BSONObjBuilder* bob) { - bob->append("specifiedAs", annotation.annotation); + // Omit 'specifiedAs' if we are generating a truncated error. + if (_context->truncate) { + return; + } + // Since this function can append values that are proportional to the size of the + // original validator expression, verify that the current builders do not exceed the + // maximum allowed validation error size. + _context->verifySizeAndAppend(annotation.annotation, "specifiedAs", bob); } void appendErrorDetails(const MatchExpression& expr) { auto annotation = expr.getErrorAnnotation(); @@ -779,7 +861,8 @@ private: BSONMatchableDocument doc(_context->getCurrentDocument()); MatchableDocument::IteratorHolder cursor(&doc, &path); BSONArrayBuilder bab; - while (cursor->more()) { + auto maxConsideredElements = _context->kMaxConsideredValuesElements; + while (cursor->more() && bab.arrSize() < maxConsideredElements) { auto elem = cursor->next().element(); if (elem.eoo()) { break; @@ -787,6 +870,15 @@ private: bab.append(elem); } } + + // Indicate that 'consideredValues' has been truncated if there are non eoo elements left + // in 'cursor'. + if (cursor->more() && bab.arrSize() == maxConsideredElements) { + auto elem = cursor->next().element(); + if (!elem.eoo()) { + _context->markConsideredValuesAsTruncated(); + } + } return bab.arr(); } @@ -867,22 +959,27 @@ private: } } void appendConsideredValue(const BSONArray& array) { - _context->getCurrentObjBuilder().append("consideredValue"_sd, array); + _context->verifySizeAndAppend(array, "consideredValue", &_context->getCurrentObjBuilder()); } void appendConsideredValues(const BSONArray& arr) { int size = arr.nFields(); - if (size == 0) { - return; // there are no values to append + // Return if there are no values to append or if we are generating a truncated error. + if (size == 0 || _context->truncate) { + return; } BSONObjBuilder& bob = _context->getCurrentObjBuilder(); if (size == 1) { - bob.appendAs(arr[0], "consideredValue"); + _context->verifySizeAndAppendAs(arr[0], "consideredValue", &bob); } else { - bob.append("consideredValues", arr); + _context->verifySizeAndAppend(arr, "consideredValues", &bob); + } + + if (_context->isConsideredValuesTruncated()) { + bob.append("consideredValuesTruncated", true); } } void appendConsideredTypes(const BSONArray& arr) { - if (arr.nFields() == 0) { + if (arr.isEmpty()) { return; // no values means no considered types } BSONObjBuilder& bob = _context->getCurrentObjBuilder(); @@ -1187,7 +1284,8 @@ private: while (it.more()) { detailsArrayBuilder.append(it.next()); } - _context->getCurrentObjBuilder().append("additionalItems"_sd, detailsArrayBuilder.arr()); + _context->verifySizeAndAppend( + detailsArrayBuilder.arr(), "additionalItems", &_context->getCurrentObjBuilder()); } /** @@ -1580,32 +1678,14 @@ bool hasErrorAnnotations(const MatchExpression& validatorExpr) { } /** - * Generates a document validation error using match expression 'validatorExpr' for document - * 'doc'. + * Appends the object id of 'doc' to 'builder' under the 'failingDocumentId' field. */ -BSONObj generateDocumentValidationError(const MatchExpression& validatorExpr, const BSONObj& doc) { - ValidationErrorContext context(doc); - ValidationErrorPreVisitor preVisitor{&context}; - ValidationErrorInVisitor inVisitor{&context}; - ValidationErrorPostVisitor postVisitor{&context}; - - // TODO SERVER-49446: Once all nodes have ErrorAnnotations, this check should be converted to an - // invariant check that all nodes have an annotation. Also add an invariant to the - // DocumentValidationFailureInfo constructor to check that it is initialized with a non-empty - // object. - if (!hasErrorAnnotations(validatorExpr)) { - return BSONObj(); - } - MatchExpressionWalker walker{&preVisitor, &inVisitor, &postVisitor}; - tree_walker::walk<true, MatchExpression>(&validatorExpr, &walker); - - // There should be no frames when error generation is complete as the finished error will be - // stored in 'context'. - invariant(context.frames.empty()); - auto error = context.getLatestCompleteErrorObject(); - invariant(!error.isEmpty()); - return error; +void appendDocumentId(const BSONObj& doc, BSONObjBuilder* builder) { + BSONElement objectIdElement; + invariant(doc.getObjectID(objectIdElement)); + builder->appendAs(objectIdElement, "failingDocumentId"_sd); } + /** * Returns true if 'generatedError' is of valid depth; false otherwise. */ @@ -1628,6 +1708,63 @@ bool checkValidationErrorDepth(const BSONObj& generatedError) { } return true; } + +/** + * Generates a document validation error using match expression 'validatorExpr' for document + * 'doc'. + */ +BSONObj generateErrorHelper(const MatchExpression& validatorExpr, + const BSONObj& doc, + bool truncate, + const int maxDocValidationErrorSize, + const int maxConsideredValues) { + // Throw if 'docValidationInternalErrorFailPoint' is enabled. + uassert(4944300, + "docValidationInternalErrorFailPoint is enabled", + !docValidationInternalErrorFailPoint.shouldFail()); + + ValidationErrorContext context(doc, truncate, maxDocValidationErrorSize, maxConsideredValues); + ValidationErrorPreVisitor preVisitor{&context}; + ValidationErrorInVisitor inVisitor{&context}; + ValidationErrorPostVisitor postVisitor{&context}; + + // TODO SERVER-49446: Once all nodes have ErrorAnnotations, this check should be converted to an + // invariant check that all nodes have an annotation. Also add an invariant to the + // DocumentValidationFailureInfo constructor to check that it is initialized with a non-empty + // object. + if (!hasErrorAnnotations(validatorExpr)) { + return BSONObj(); + } + MatchExpressionWalker walker{&preVisitor, &inVisitor, &postVisitor}; + tree_walker::walk<true, MatchExpression>(&validatorExpr, &walker); + + // There should be no frames when error generation is complete as the finished error will be + // stored in 'context'. + invariant(context.frames.empty()); + auto error = context.getLatestCompleteErrorObject(); + invariant(!error.isEmpty()); + + // Add document id to the error object. + BSONObjBuilder objBuilder; + appendDocumentId(doc, &objBuilder); + + // Record whether the generated error was truncated. + if (truncate) + objBuilder.append("truncated", true); + // Add errors from match expressions. + objBuilder.append("details"_sd, std::move(error)); + + auto finalError = objBuilder.obj(); + // Verify that the generated error is of valid depth. + if (!checkValidationErrorDepth(finalError)) { + BSONObjBuilder errorDetails; + static constexpr auto kDeeplyNestedError = "generated error was too deeply nested"; + errorDetails.append("reason", kDeeplyNestedError); + errorDetails.append("truncated", true); + return errorDetails.obj(); + } + return finalError; +} } // namespace std::shared_ptr<const ErrorExtraInfo> DocumentValidationFailureInfo::parse(const BSONObj& obj) { @@ -1649,27 +1786,43 @@ const BSONObj& DocumentValidationFailureInfo::getDetails() const { return _details; } -BSONObj generateError(const MatchExpression& validatorExpr, const BSONObj& doc) { - auto error = generateDocumentValidationError(validatorExpr, doc); - BSONObjBuilder objBuilder; - - // Add document id to the error object. - BSONElement objectIdElement; - invariant(doc.getObjectID(objectIdElement)); - objBuilder.appendAs(objectIdElement, "failingDocumentId"_sd); - - // Add errors from match expressions. - objBuilder.append("details"_sd, std::move(error)); - auto finalError = objBuilder.obj(); - - // Verify that the generated error is of valid depth. - if (!checkValidationErrorDepth(finalError)) { - BSONObjBuilder errorDetails; - static constexpr auto kDeeplyNestedError = "generated error was too deeply nested"; - errorDetails.append("reason", kDeeplyNestedError); - errorDetails.append("truncated", true); - return errorDetails.obj(); - } - return finalError; +BSONObj generateError(const MatchExpression& validatorExpr, + const BSONObj& doc, + const int maxDocValidationErrorSize, + const int maxConsideredValues) { + // Attempt twice to generate a detailed document validation error before reporting to the user + // that the generated error grew too large. + constexpr static auto kNoteString = "note"; + bool truncate = false; + for (auto attempt = 0; attempt < 2; ++attempt) { + try { + auto error = generateErrorHelper( + validatorExpr, doc, truncate, maxDocValidationErrorSize, maxConsideredValues); + uassert(ErrorCodes::BSONObjectTooLarge, + "doc validation error exceeded maximum size", + error.objsize() <= maxDocValidationErrorSize); + return error; + } catch (const ExceptionFor<ErrorCodes::BSONObjectTooLarge>&) { + // Try again, but this time omit details such as 'consideredValues' or 'specifiedAs' + // that are proportional to the size of the validator expression or the failed document. + truncate = true; + } catch (const DBException& e) { + BSONObjBuilder error; + appendDocumentId(doc, &error); + static constexpr auto kErrorReason = "failed to generate document validation error"; + error.append(kNoteString, kErrorReason); + BSONObjBuilder subBuilder = error.subobjStart("details"); + e.serialize(&subBuilder); + subBuilder.done(); + return error.obj(); + } + } + // If we've reached here, both attempts failed to generate a sufficiently small error. Return + // an error indicating as much to the user. + BSONObjBuilder error; + appendDocumentId(doc, &error); + static constexpr auto kTruncationReason = "detailed error was too large"; + error.append(kNoteString, kTruncationReason); + return error.obj(); } } // namespace mongo::doc_validation_error diff --git a/src/mongo/db/matcher/doc_validation_error.h b/src/mongo/db/matcher/doc_validation_error.h index ae632080179..e610138de21 100644 --- a/src/mongo/db/matcher/doc_validation_error.h +++ b/src/mongo/db/matcher/doc_validation_error.h @@ -31,8 +31,12 @@ #include "mongo/base/error_extra_info.h" #include "mongo/db/matcher/expression.h" +#include "mongo/db/query/query_knobs_gen.h" namespace mongo::doc_validation_error { +// The default maximum allowed size for a single doc validation error. +constexpr static int kDefaultMaxDocValidationErrorSize = 12 * 1024 * 1024; + /** * Represents information about a document validation error. */ @@ -53,5 +57,9 @@ private: * reference to a BSONObj corresponding to the document that failed to match against the validator * expression, returns a BSONObj that describes why 'doc' failed to match against 'validatorExpr'. */ -BSONObj generateError(const MatchExpression& validatorExpr, const BSONObj& doc); +BSONObj generateError( + const MatchExpression& validatorExpr, + const BSONObj& doc, + int maxDocValidationErrorSize = kDefaultMaxDocValidationErrorSize, + int maxConsideredValues = internalQueryMaxDocValidationErrorConsideredValues.load()); } // namespace mongo::doc_validation_error
\ No newline at end of file diff --git a/src/mongo/db/matcher/doc_validation_error_test.cpp b/src/mongo/db/matcher/doc_validation_error_test.cpp index 4a3011f6359..38b67fe5946 100644 --- a/src/mongo/db/matcher/doc_validation_error_test.cpp +++ b/src/mongo/db/matcher/doc_validation_error_test.cpp @@ -38,7 +38,11 @@ namespace { * Parses a MatchExpression from 'query' and returns a validation error generated by the parsed * MatchExpression against document 'document'. */ -BSONObj generateValidationError(const BSONObj& query, const BSONObj& document) { +BSONObj generateValidationError( + const BSONObj& query, + const BSONObj& document, + const int maxDocValidationErrorSize = kDefaultMaxDocValidationErrorSize, + const int maxConsideredValues = internalQueryMaxDocValidationErrorConsideredValues.load()) { boost::intrusive_ptr<ExpressionContextForTest> expCtx(new ExpressionContextForTest()); expCtx->isParsingCollectionValidator = true; StatusWithMatchExpression result = MatchExpressionParser::parse(query, expCtx); @@ -50,7 +54,9 @@ BSONObj generateValidationError(const BSONObj& query, const BSONObj& document) { return doc_validation_error::generateError( *expr, - document.hasField("_id") ? document : document.addField(BSON("_id" << 1).firstElement())); + document.hasField("_id") ? document : document.addField(BSON("_id" << 1).firstElement()), + maxDocValidationErrorSize, + maxConsideredValues); } } // namespace @@ -81,6 +87,50 @@ TEST(GenerateValidationError, FailingDocumentId) { expectedError); } +/** + * Utility which verifies that 'obj' has no fields from 'missingFields'. + */ +void verifyExpectedFieldsAreMissing(const BSONObj& obj, + const std::set<std::string>& missingFields) { + for (auto&& field : missingFields) { + ASSERT_FALSE(obj.hasField(field)); + } + + for (auto&& elem : obj) { + if (elem.type() == BSONType::Object) { + verifyExpectedFieldsAreMissing(elem.Obj(), missingFields); + } else if (elem.type() == BSONType::Array) { + for (auto&& arrayElem : elem.embeddedObject()) { + if (arrayElem.type() == BSONType::Object) { + verifyExpectedFieldsAreMissing(arrayElem.embeddedObject(), missingFields); + } + } + } + } +} + +/** + * Generates a document validation error for 'query' and 'document' and verifies that it is a + * truncated error. Most importantly, the generated error should be a valid BSONObj (i.e. + * does not exceed 16MB), but should have a 'truncated' field at the top level with a value + * of 'true', and there should be no 'consideredValues' or 'specifiedAs' fields within the + * error. + */ +void verifyTruncatedError(const BSONObj& query, + const BSONObj& document, + const int maxDocValidationErrorSize, + const int maxConsideredValuesSize) { + auto generatedError = generateValidationError( + query, document, maxDocValidationErrorSize, maxConsideredValuesSize); + ASSERT_TRUE(generatedError.hasField("truncated")); + auto elem = generatedError["truncated"]; + ASSERT_TRUE(elem.isBoolean()); + ASSERT_TRUE(elem.boolean()); + const std::set<std::string> missingFields{"specifiedAs", "consideredValues", "consideredValue"}; + verifyExpectedFieldsAreMissing(generatedError, missingFields); + ASSERT_LTE(generatedError.objsize(), maxDocValidationErrorSize); +} + // Comparison operators. // $eq TEST(ComparisonMatchExpression, BasicEq) { @@ -1658,6 +1708,8 @@ TEST(ArrayMatchingMatchExpression, AllOverElemMatch) { doc_validation_error::verifyGeneratedError(query, document, expectedError); } +// Truncation tests + /** * Generates a query of nested $ors that is 'desiredDepth' levels deep. */ @@ -1705,5 +1757,141 @@ TEST(ValidationErrorTruncation, BasicDeeplyNestedError) { ASSERT_FALSE(error.hasField("truncated")); ASSERT_EQ(error.getField("details").type(), BSONType::Object); } + +TEST(DocValidationTruncationTest, TruncatedConsideredValuesArray) { + // Build a large array of numbers. + BSONObj query = BSON("a" << BSON("$lt" << 0)); + BSONArrayBuilder valuesArray; + BSONArrayBuilder consideredValues; + const auto consideredValuesLimit = internalQueryMaxDocValidationErrorConsideredValues.load(); + constexpr int kNumElements = 1000; + for (int i = 0; i < kNumElements; i++) { + valuesArray.append(i); + if (i < consideredValuesLimit) { + consideredValues.append(i); + } + } + BSONObj document = BSON("a" << valuesArray.arr()); + BSONObj expectedError = BSON("operatorName" + << "$lt" + << "specifiedAs" << query << "reason" + << "comparison failed" + << "consideredValues" << consideredValues.arr() + << "consideredValuesTruncated" << true); + verifyGeneratedError(query, document, expectedError); +} + +TEST(DocValidationTruncationTest, ConsideredValuesArrayNotTruncatedWhenConfiguredLimitMatches) { + BSONObj query = BSON("a" << BSON("$lt" << 0)); + BSONArrayBuilder valuesArray; + // TODO SERVER-49454: We subtract one to account for the fact that the ElementIterator + // interface will report the entire array as the last element in 'consideredValues'. This + // subtraction should be removed if the interface is updated to allow for omitting the array + // at the end. + auto consideredValuesLimit = internalQueryMaxDocValidationErrorConsideredValues.load() - 1; + for (int i = 0; i < consideredValuesLimit; i++) { + valuesArray.append(i); + } + BSONObj document = BSON("a" << valuesArray.arr()); + BSONObj generatedError = generateValidationError(query, document); + // Should not report 'consideredValuesTruncated: true' + ASSERT_FALSE(generatedError.hasField("consideredValuesTruncated")); +} + +TEST(DocValidationTruncationTest, BasicSizeTruncation) { + // Configure error generation to use a maximum error size of 10KB. This allows for us to test + // truncation behavior without constructing massive errors. + static int maxSize = 10 * 1024; + // Target size to use for constructing large queries/documents. + static int targetSize = 10 * 1024; + // Constructs an array of numbers of the desired size in bytes. + auto buildNumberArray = [](int sizeInBytes, int value) -> BSONArray { + BSONArrayBuilder builder; + while (builder.len() < sizeInBytes) { + builder.append(value); + ++value; + } + return builder.arr(); + }; + + // $in predicate that is of size 'maxSize' gets omitted from error output. + { + BSONObj query = BSON("a" << BSON("$in" << buildNumberArray(targetSize, 1))); + BSONObj doc = BSON("a" << 0); + verifyTruncatedError( + query, doc, maxSize, internalQueryMaxDocValidationErrorConsideredValues.load()); + } + + // Document which contains an array of size 'maxSize' will result in 'consideredValues' field + // being omitted. + { + auto numberArray = buildNumberArray(targetSize, 1); + BSONObj query = fromjson("{a: {$in: [0]}}"); + BSONObj doc = BSON("a" << numberArray); + // We use the number of elements returned here to ensure that none of the values in the + // generated array get truncated. + verifyTruncatedError(query, doc, maxSize, numberArray.nFields()); + } + + // Construct the $in predicate and the array of values such that each have a size in bytes of + // 7 KB that is under the 10KB size limit, but their combined size exceeds the configured size + // limit of 10KB. + targetSize = 7 * 1024; + { + auto queryArray = buildNumberArray(targetSize, 1); + BSONObj largeQuery = BSON("a" << BSON("$in" << queryArray)); + auto docArray = buildNumberArray(targetSize, queryArray.nFields() + 1); + BSONObj largeDoc = BSON("a" << docArray); + // Both the query and the doc should be underneath 'maxSize' individually. + ASSERT_LESS_THAN(largeQuery.objsize(), maxSize); + ASSERT_LESS_THAN(largeDoc.objsize(), maxSize); + // The combined size exceeds 'maxSize' and will result in a truncated error. + verifyTruncatedError(largeQuery, largeDoc, maxSize, docArray.nFields()); + } + + // The number of $eq predicates is large enough that we will not be able to generate a detailed + // error as the size of the truncated error details will dwarf the 1 KB limit. + maxSize = 1024; + { + const int numClauses = 1000; + BSONObj doc = BSON("a" << 0); + BSONArrayBuilder andArgument; + for (auto i = 1; i < numClauses; ++i) { + andArgument.append(BSON("a" << i)); + } + BSONObj query = BSON("$and" << andArgument.arr()); + auto error = generateValidationError( + query, doc, maxSize, internalQueryMaxDocValidationErrorConsideredValues.load()); + // Generated error should have a note field and a failingDocumentId field, but no details + // field. + auto note = error["note"]; + ASSERT_EQ(note.type(), BSONType::String); + ASSERT_EQ(note.valueStringData(), "detailed error was too large"); + ASSERT_TRUE(error.hasField("failingDocumentId")); + ASSERT_FALSE(error.hasField("details")); + } +} + +TEST(DocValidationTruncationTest, DocValidationReportsProgrammingError) { + // An internal error thrown by the doc_validation_error module should be caught and handled + // appropriately. + FailPointEnableBlock fp("docValidationInternalErrorFailPoint"); + BSONObj query = BSON("a" << 1); + BSONObj doc = BSON("b" << 4); + auto error = generateValidationError(query, + doc, + kDefaultMaxDocValidationErrorSize, + internalQueryMaxDocValidationErrorConsideredValues.load()); + // There should be an error with 'note' and 'details' fields. + auto note = error["note"]; + ASSERT_EQ(note.type(), BSONType::String); + ASSERT_EQ(note.valueStringData(), "failed to generate document validation error"); + ASSERT_TRUE(error.hasField("failingDocumentId")); + auto details = error["details"]; + ASSERT_EQ(details.type(), BSONType::Object); + auto code = details.embeddedObject()["code"]; + ASSERT_EQ(code.type(), BSONType::NumberInt); + ASSERT_EQ(code.numberInt(), 4944300); +} } // namespace } // namespace mongo::doc_validation_error diff --git a/src/mongo/db/query/query_knobs.idl b/src/mongo/db/query/query_knobs.idl index dccf2086e1b..5dc83d76b47 100644 --- a/src/mongo/db/query/query_knobs.idl +++ b/src/mongo/db/query/query_knobs.idl @@ -416,3 +416,14 @@ server_parameters: cpp_varname: "internalQueryEnableCSTParser" cpp_vartype: AtomicWord<bool> default: false + + internalQueryMaxDocValidationErrorConsideredValues: + description: "Limits the number of values reported in the 'consideredValues' array when + generating a descriptive document validation error." + set_at: [ startup, runtime ] + cpp_varname: "internalQueryMaxDocValidationErrorConsideredValues" + cpp_vartype: AtomicWord<int> + default: + expr: 10 + validator: + gt: 0 |