summaryrefslogtreecommitdiff
path: root/src/mongo/db
diff options
context:
space:
mode:
authorMihai Andrei <mihai.andrei@10gen.com>2020-10-08 16:37:52 -0400
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2020-10-09 03:05:45 +0000
commitde8c206fd8c2a8c1333d07e2d9b8fd481eb37131 (patch)
treed7eae7047ab0e7a0c5c980ba658e5d10f65396b9 /src/mongo/db
parent11d461d8335e5b63d2bd2d5509dd32b0775f1700 (diff)
downloadmongo-de8c206fd8c2a8c1333d07e2d9b8fd481eb37131.tar.gz
SERVER-49443 Implement error truncation for generated document validation errors
Diffstat (limited to 'src/mongo/db')
-rw-r--r--src/mongo/db/matcher/doc_validation_error.cpp277
-rw-r--r--src/mongo/db/matcher/doc_validation_error.h10
-rw-r--r--src/mongo/db/matcher/doc_validation_error_test.cpp192
-rw-r--r--src/mongo/db/query/query_knobs.idl11
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..30ca2bbc1d8 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'.
@@ -220,6 +235,46 @@ struct ValidationErrorContext {
frames.top().inversion = inversion;
}
+ /**
+ * 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);
+ }
+
bool haveLatestCompleteError() {
return !stdx::holds_alternative<std::monostate>(latestCompleteError);
}
@@ -228,10 +283,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 +303,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
@@ -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