diff options
| author | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-14 14:26:38 -0300 |
|---|---|---|
| committer | Lucas de Castro Borges <lucas@gnuabordo.com.br> | 2025-02-14 14:26:38 -0300 |
| commit | 294bc6ecabf14c09c9bc8644704921dcf97cb44e (patch) | |
| tree | 279b1e0bab53901a1647ac63c1c724f0f789a663 /src/mongo/db/pipeline | |
| parent | 70be7c27a251621187a1de533462ae2bb1e3bd39 (diff) | |
| parent | 1e917fd798aa25b7066d4b414b51184f13d5a092 (diff) | |
Update upstream source from tag 'upstream/6.0.10'debian/6.0.10-1
Update to upstream version '6.0.10'
with Debian dir 2d176fa254eee97b139f712fec5709641335a8c3
Diffstat (limited to 'src/mongo/db/pipeline')
158 files changed, 6914 insertions, 2576 deletions
diff --git a/src/mongo/db/pipeline/SConscript b/src/mongo/db/pipeline/SConscript index 16e39fc4832..a1c0634027e 100644 --- a/src/mongo/db/pipeline/SConscript +++ b/src/mongo/db/pipeline/SConscript @@ -104,6 +104,7 @@ env.Library( 'expression_trigonometric.cpp', 'javascript_execution.cpp', 'make_js_function.cpp', + 'monotonic_expression.cpp', 'variables.cpp', ], LIBDEPS=[ @@ -165,6 +166,7 @@ env.Library( 'accumulator_rank.cpp', 'accumulator_std_dev.cpp', 'accumulator_sum.cpp', + 'map_reduce_options.idl', 'window_function/window_bounds.cpp', 'window_function/window_function_covariance.cpp', 'window_function/window_function_count.cpp', @@ -183,8 +185,9 @@ env.Library( ], LIBDEPS_PRIVATE=[ '$BUILD_DIR/mongo/db/exec/sort_executor', - '$BUILD_DIR/mongo/db/index/key_generator' - ] + '$BUILD_DIR/mongo/db/index/index_access_method', + '$BUILD_DIR/mongo/idl/idl_parser', + ], ) env.Library( @@ -287,7 +290,9 @@ pipelineEnv.Library( 'document_source_geo_near.cpp', 'document_source_graph_lookup.cpp', 'document_source_group.cpp', + 'document_source_group_base.cpp', 'document_source_index_stats.cpp', + 'document_source_internal_all_collection_stats.cpp', 'document_source_internal_compute_geo_near_distance.cpp', 'document_source_internal_convert_bucket_index_stats.cpp', 'document_source_internal_inhibit_optimization.cpp', @@ -314,13 +319,16 @@ pipelineEnv.Library( 'document_source_sequential_document_cache.cpp', 'document_source_set_variable_from_subpipeline.cpp', 'document_source_set_window_fields.cpp', + 'document_source_sharded_data_distribution.cpp', 'document_source_single_document_transformation.cpp', 'document_source_skip.cpp', 'document_source_sort.cpp', 'document_source_sort_by_count.cpp', + 'document_source_streaming_group.cpp', 'document_source_tee_consumer.cpp', 'document_source_union_with.cpp', 'document_source_unwind.cpp', + 'group_from_first_document_transformation.cpp', 'pipeline.cpp', 'search_helper.cpp', 'semantic_analysis.cpp', @@ -349,7 +357,7 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/db/exec/scoped_timer', '$BUILD_DIR/mongo/db/exec/sort_executor', '$BUILD_DIR/mongo/db/generic_cursor', - '$BUILD_DIR/mongo/db/index/key_generator', + '$BUILD_DIR/mongo/db/index/index_access_method', '$BUILD_DIR/mongo/db/logical_session_cache', '$BUILD_DIR/mongo/db/logical_session_id_helpers', '$BUILD_DIR/mongo/db/matcher/expressions', @@ -377,6 +385,7 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/s/is_mongos', '$BUILD_DIR/third_party/shim_snappy', 'accumulator', + 'change_stream_helpers', 'dependencies', 'document_path_support', 'document_sources_idl', @@ -391,6 +400,7 @@ pipelineEnv.Library( '$BUILD_DIR/mongo/db/query/projection_ast', '$BUILD_DIR/mongo/db/repl/image_collection_entry', '$BUILD_DIR/mongo/db/sorter/sorter_idl', + '$BUILD_DIR/mongo/db/sorter/sorter_stats', '$BUILD_DIR/mongo/db/timeseries/timeseries_conversion_util', '$BUILD_DIR/mongo/db/timeseries/timeseries_options', '$BUILD_DIR/mongo/rpc/command_status', @@ -420,6 +430,7 @@ env.Library( 'change_stream_filter_helpers.cpp', 'change_stream_helpers_legacy.cpp', 'change_stream_rewrite_helpers.cpp', + 'change_stream_split_event_helpers.cpp', 'document_source_change_stream.cpp', 'document_source_change_stream_add_post_image.cpp', 'document_source_change_stream_check_invalidate.cpp', @@ -429,6 +440,7 @@ env.Library( 'document_source_change_stream_handle_topology_change.cpp', 'document_source_change_stream_add_pre_image.cpp', 'document_source_change_stream_oplog_match.cpp', + 'document_source_change_stream_split_large_event.cpp', 'document_source_change_stream_transform.cpp', 'document_source_change_stream_unwind_transaction.cpp', ], @@ -439,6 +451,7 @@ env.Library( '$BUILD_DIR/mongo/db/pipeline/sharded_agg_helpers', '$BUILD_DIR/mongo/db/update/update_driver', '$BUILD_DIR/mongo/s/query/router_exec_stage', + 'change_stream_helpers', 'change_stream_preimage', ], ) @@ -461,6 +474,7 @@ env.Library( 'document_source_coll_stats.idl', 'document_source_densify.idl', 'document_source_fill.idl', + 'document_source_internal_all_collection_stats.idl', 'document_source_internal_apply_oplog_update.idl', 'document_source_list_sessions.idl', 'document_source_merge.idl', @@ -493,6 +507,7 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/db/change_stream_options_manager', + '$BUILD_DIR/mongo/db/concurrency/exception_util', '$BUILD_DIR/mongo/db/db_raii', '$BUILD_DIR/mongo/db/query_exec', '$BUILD_DIR/mongo/db/record_id_helpers', @@ -521,6 +536,17 @@ env.Library( ) env.Library( + target="change_stream_helpers", + source=[ + "change_stream_helpers.cpp", + ], + LIBDEPS=[ + "document_sources_idl", + ], + LIBDEPS_PRIVATE=[], +) + +env.Library( target="change_stream_test_helpers", source=[ "change_stream_test_helpers.cpp", @@ -542,12 +568,14 @@ env.CppUnitTest( 'change_stream_event_transform_test.cpp', 'change_stream_expired_pre_image_remover_test.cpp', 'change_stream_rewrites_test.cpp', + 'change_stream_split_event_helpers_test.cpp', 'dependencies_test.cpp', 'dispatch_shard_pipeline_test.cpp', 'document_path_support_test.cpp', 'document_source_add_fields_test.cpp', 'document_source_bucket_auto_test.cpp', 'document_source_bucket_test.cpp', + 'document_source_change_stream_add_post_image_test.cpp', 'document_source_change_stream_test.cpp', 'document_source_check_resume_token_test.cpp', 'document_source_count_test.cpp', @@ -563,7 +591,6 @@ env.CppUnitTest( 'document_source_internal_shard_filter_test.cpp', 'document_source_internal_split_pipeline_test.cpp', 'document_source_limit_test.cpp', - 'document_source_change_stream_add_post_image_test.cpp', 'document_source_lookup_test.cpp', 'document_source_match_test.cpp', 'document_source_merge_cursors_test.cpp', @@ -619,6 +646,7 @@ env.CppUnitTest( 'granularity_rounder_preferred_numbers_test.cpp', 'lookup_set_cache_test.cpp', 'memory_usage_tracker_test.cpp', + 'monotonic_expression_test.cpp', 'partition_key_comparator_test.cpp', 'pipeline_metadata_tree_test.cpp', 'pipeline_test.cpp', diff --git a/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp b/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp index 582f3263bfb..1d136a098ce 100644 --- a/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp +++ b/src/mongo/db/pipeline/abt/abt_document_source_visitor.cpp @@ -179,7 +179,7 @@ private: } void processProjectedPaths(const projection_executor::InclusionNode& node) { - std::set<std::string> preservedPaths; + OrderedPathSet preservedPaths; node.reportProjectedPaths(&preservedPaths); for (const std::string& preservedPathStr : preservedPaths) { @@ -194,7 +194,7 @@ private: void processComputedPaths(const projection_executor::InclusionNode& node, const std::string& rootProjection, const bool isAddingFields) { - std::set<std::string> computedPaths; + OrderedPathSet computedPaths; StringMap<std::string> renamedPaths; node.reportComputedPaths(&computedPaths, &renamedPaths); @@ -268,7 +268,7 @@ private: } void visitExclusionNode(const projection_executor::ExclusionNode& node) { - std::set<std::string> preservedPaths; + OrderedPathSet preservedPaths; node.reportProjectedPaths(&preservedPaths); for (const std::string& preservedPathStr : preservedPaths) { diff --git a/src/mongo/db/pipeline/abt/field_map_builder.h b/src/mongo/db/pipeline/abt/field_map_builder.h index af593d38cd3..4eedf150644 100644 --- a/src/mongo/db/pipeline/abt/field_map_builder.h +++ b/src/mongo/db/pipeline/abt/field_map_builder.h @@ -57,7 +57,7 @@ struct FieldMapEntry { bool _hasDrop = false; std::string _constVarName; - std::set<std::string> _childPaths; + OrderedPathSet _childPaths; }; class FieldMapBuilder { diff --git a/src/mongo/db/pipeline/accumulation_statement.cpp b/src/mongo/db/pipeline/accumulation_statement.cpp index d5afb1f83d0..cb5efcf81ec 100644 --- a/src/mongo/db/pipeline/accumulation_statement.cpp +++ b/src/mongo/db/pipeline/accumulation_statement.cpp @@ -38,6 +38,7 @@ #include "mongo/db/exec/document_value/value.h" #include "mongo/db/pipeline/accumulator.h" #include "mongo/db/query/allowed_contexts.h" +#include "mongo/db/stats/counters.h" #include "mongo/util/assert_util.h" #include "mongo/util/str.h" #include "mongo/util/string_map.h" @@ -63,6 +64,7 @@ void AccumulationStatement::registerAccumulator( str::stream() << "Duplicate accumulator (" << name << ") registered.", it == parserMap.end()); parserMap[name] = {parser, allowedWithApiStrict, allowedWithClientType, requiredMinVersion}; + operatorCountersGroupAccumulatorExpressions.addCounter(name); } AccumulationStatement::ParserRegistration& AccumulationStatement::getParser(StringData name) { @@ -118,6 +120,8 @@ AccumulationStatement AccumulationStatement::parseAccumulationStatement( tassert(5837900, "Accumulators should only appear in a user operation", expCtx->opCtx); assertLanguageFeatureIsAllowed( expCtx->opCtx, accName.toString(), allowedWithApiStrict, allowedWithClientType); + + expCtx->incrementGroupAccumulatorExprCounter(accName); auto accExpr = parser(expCtx, specElem, vps); return AccumulationStatement(fieldName.toString(), std::move(accExpr)); diff --git a/src/mongo/db/pipeline/accumulator_js_reduce.cpp b/src/mongo/db/pipeline/accumulator_js_reduce.cpp index 71075978d6d..78058d5913d 100644 --- a/src/mongo/db/pipeline/accumulator_js_reduce.cpp +++ b/src/mongo/db/pipeline/accumulator_js_reduce.cpp @@ -32,6 +32,7 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/pipeline/accumulator_js_reduce.h" #include "mongo/db/pipeline/make_js_function.h" +#include "mongo/db/pipeline/map_reduce_options_gen.h" namespace mongo { @@ -120,47 +121,52 @@ Value AccumulatorInternalJsReduce::getValue(bool toBeMerged) { if (_values.size() < 1) { return Value{}; } - - const auto keySize = _key.getApproximateSize(); - Value result; - // Keep reducing until we have exactly one value. - while (true) { - BSONArrayBuilder bsonValues; - size_t numLeft = _values.size(); - for (; numLeft > 0; numLeft--) { - Value val = _values[numLeft - 1]; - - // Do not insert if doing so would exceed the the maximum allowed BSONObj size. - if (bsonValues.len() + keySize + val.getApproximateSize() > BSONObjMaxUserSize) { - // If we have reached the threshold for maximum allowed BSONObj size and only have a - // single value then no progress will be made on reduce. We must fail when this - // scenario is encountered. - size_t numNextReduce = _values.size() - numLeft; - uassert(31392, "Value too large to reduce", numNextReduce > 1); - break; + if (mrSingleReduceOptimizationEnabled && _values.size() == 1) { + // This optimization existed in the old Pre-4.4 MapReduce implementation. If the flag is + // set, then we should replicate the optimization. See SERVER-68766 for more details. + result = std::move(_values[0]); + } else { + const auto keySize = _key.getApproximateSize(); + + // Keep reducing until we have exactly one value. + while (true) { + BSONArrayBuilder bsonValues; + size_t numLeft = _values.size(); + for (; numLeft > 0; numLeft--) { + Value val = _values[numLeft - 1]; + + // Do not insert if doing so would exceed the the maximum allowed BSONObj size. + if (bsonValues.len() + keySize + val.getApproximateSize() > BSONObjMaxUserSize) { + // If we have reached the threshold for maximum allowed BSONObj size and only + // have a single value then no progress will be made on reduce. We must fail + // when this scenario is encountered. + size_t numNextReduce = _values.size() - numLeft; + uassert(31392, "Value too large to reduce", numNextReduce > 1); + break; + } + bsonValues << val; } - bsonValues << val; - } - auto expCtx = getExpressionContext(); - auto reduceFunc = makeJsFunc(expCtx, _funcSource); - - // Function signature: reduce(key, values). - BSONObj params = BSON_ARRAY(_key << bsonValues.arr()); - // For reduce, the key and values are both passed as 'params' so there's no need to set - // 'this'. - BSONObj thisObj; - Value reduceResult = - expCtx->getJsExecWithScope()->callFunction(reduceFunc, params, thisObj); - if (numLeft == 0) { - result = reduceResult; - break; - } else { - // Remove all values which have been reduced. - _values.resize(numLeft); - // Include most recent result in the set of values to be reduced. - _values.push_back(reduceResult); + auto expCtx = getExpressionContext(); + auto reduceFunc = makeJsFunc(expCtx, _funcSource); + + // Function signature: reduce(key, values). + BSONObj params = BSON_ARRAY(_key << bsonValues.arr()); + // For reduce, the key and values are both passed as 'params' so there's no need to set + // 'this'. + BSONObj thisObj; + Value reduceResult = + expCtx->getJsExecWithScope()->callFunction(reduceFunc, params, thisObj); + if (numLeft == 0) { + result = reduceResult; + break; + } else { + // Remove all values which have been reduced. + _values.resize(numLeft); + // Include most recent result in the set of values to be reduced. + _values.push_back(reduceResult); + } } } diff --git a/src/mongo/db/pipeline/accumulator_js_test.cpp b/src/mongo/db/pipeline/accumulator_js_test.cpp index 953b4ce475a..2a44e8e967c 100644 --- a/src/mongo/db/pipeline/accumulator_js_test.cpp +++ b/src/mongo/db/pipeline/accumulator_js_test.cpp @@ -38,6 +38,7 @@ #include "mongo/db/pipeline/process_interface/standalone_process_interface.h" #include "mongo/db/service_context_d_test_fixture.h" #include "mongo/dbtests/dbtests.h" +#include "mongo/idl/server_parameter_test_util.h" #include "mongo/scripting/engine.h" namespace mongo { @@ -197,6 +198,38 @@ TEST_F(MapReduceFixture, InternalJsReduceFailsWhenEvalContainsInvalidJavascript) } } +TEST_F( + MapReduceFixture, + InternalJsReduceFailsDependentOnDocumentCountWhenEvalIsInvalidJavascriptWithSingleReduceOpt) { + RAIIServerParameterControllerForTest flag("mrEnableSingleReduceOptimization", true); + std::string eval("INVALID_JAVASCRIPT"); + // Multiple source documents should evaluate the passed in function and return an error with + // invalid javascript. + { + auto accum = AccumulatorInternalJsReduce::create(getExpCtx(), "INVALID_JAVASCRIPT"); + auto input = Value(DOC("k" << Value(1) << "v" << Value(2))); + accum->process(input, false); + accum->process(input, false); + + ASSERT_THROWS_CODE(accum->getValue(false), DBException, ErrorCodes::JSInterpreterFailure); + } + + // Single source document. With the reduce optimization, we simply return this document rather + // than executing the JS engine at all, so no error is thrown. + { + auto accum = AccumulatorInternalJsReduce::create(getExpCtx(), "INVALID_JAVASCRIPT"); + + auto input = Value(DOC("k" << Value(1) << "v" << Value(2))); + auto expectedResult = Value(2); + + accum->process(input, false); + Value result = accum->getValue(false); + + ASSERT_VALUE_EQ(expectedResult, result); + ASSERT_EQUALS(expectedResult.getType(), result.getType()); + } +} + TEST_F(MapReduceFixture, InternalJsReduceFailsIfArgumentNotDocument) { auto argument = Value(2); assertProcessFailsWithCode<AccumulatorInternalJsReduce>( diff --git a/src/mongo/db/pipeline/accumulator_multi.cpp b/src/mongo/db/pipeline/accumulator_multi.cpp index 2f6971e1f22..57cf06f0090 100644 --- a/src/mongo/db/pipeline/accumulator_multi.cpp +++ b/src/mongo/db/pipeline/accumulator_multi.cpp @@ -671,12 +671,6 @@ void AccumulatorTopBottomN<sense, single>::_processValue(const Value& val) { } } - // TODO SERVER-61281 consider removing this call to fillCache(). - // Since Document caches fields the size of this cache and getApproximateSize() can vary - // depending on access. In order to avoid this and make sure we subtract the right amount if - // remove() ever gets called, we can fill the cache to get a consistent view of the size. - // Normally the outer window function code handles this, but _genKeyOutPair() makes a new - // document for sortKey, so its cache get reset. keyOutPair.first.fillCache(); const auto memUsage = keyOutPair.first.getApproximateSize() + keyOutPair.second.getApproximateSize() + sizeof(KeyOutPair); @@ -698,9 +692,6 @@ void AccumulatorTopBottomN<sense, single>::remove(const Value& val) { auto it = _map->lower_bound(keyOutPair.first); _map->erase(it); - // TODO SERVER-61281 consider removing this comment if its no longer relevant. - // After calling lower_bound() it uses SortKeyComparator and the sortKey's field cache should be - // fully populated so no need to call fillCache() again. _memUsageBytes -= keyOutPair.first.getApproximateSize() + keyOutPair.second.getApproximateSize() + sizeof(KeyOutPair); } diff --git a/src/mongo/db/pipeline/aggregate_command.idl b/src/mongo/db/pipeline/aggregate_command.idl index b84513a30e6..0a4ae1f7ec4 100644 --- a/src/mongo/db/pipeline/aggregate_command.idl +++ b/src/mongo/db/pipeline/aggregate_command.idl @@ -156,6 +156,14 @@ commands: - privilege: # $backupCursorExtend, backupCursor resource_pattern: cluster action_type: fsync + - privilege: # $_internalAllCollectionStats + agg_stage: _internalAllCollectionStats + resource_pattern: cluster + action_type: allCollectionStats + - privilege: # $shardedDataDistribution + agg_stage: shardedDataDistribution + resource_pattern: cluster + action_type: shardedDataDistribution # Note that the 'CursorInitialReply' is not the only response that an aggregate command # could return. With 'explain' or 'exchange', the response would not include the fields in # 'CursorInitialReply'. But using 'explain' or 'exchange' is unstable, but otherwise the diff --git a/src/mongo/db/pipeline/change_stream_event_transform.cpp b/src/mongo/db/pipeline/change_stream_event_transform.cpp index 9e8a7d8ac19..f8b5052ecbe 100644 --- a/src/mongo/db/pipeline/change_stream_event_transform.cpp +++ b/src/mongo/db/pipeline/change_stream_event_transform.cpp @@ -31,6 +31,8 @@ #include "mongo/db/pipeline/change_stream_document_diff_parser.h" #include "mongo/db/pipeline/change_stream_filter_helpers.h" +#include "mongo/db/pipeline/change_stream_helpers.h" +#include "mongo/db/pipeline/change_stream_helpers_legacy.h" #include "mongo/db/pipeline/change_stream_preimage_gen.h" #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/document_source_change_stream_add_post_image.h" @@ -43,6 +45,7 @@ namespace mongo { namespace { constexpr auto checkValueType = &DocumentSourceChangeStream::checkValueType; +constexpr auto resolveResumeToken = &change_stream::resolveResumeTokenFromSpec; Document copyDocExceptFields(const Document& source, const std::set<StringData>& fieldNames) { MutableDocument doc(source); @@ -79,8 +82,7 @@ ChangeStreamEventTransformation::ChangeStreamEventTransformation( const DocumentSourceChangeStreamSpec& spec) : _changeStreamSpec(spec), _expCtx(expCtx) { // Extract the resume token from the spec and store it. - _resumeToken = - DocumentSourceChangeStream::resolveResumeTokenFromSpec(_expCtx, _changeStreamSpec); + _resumeToken = resolveResumeToken(_expCtx, _changeStreamSpec); // Determine whether the user requested a point-in-time pre-image, which will affect this // stage's output. @@ -107,8 +109,10 @@ ResumeTokenData ChangeStreamEventTransformation::makeResumeToken(Value tsVal, // If we have a resume token, we need to match the version with which it was generated until we // have surpassed it, at which point we can begin generating tokens with our default version. - auto version = (clusterTime > _resumeToken.clusterTime) ? _expCtx->changeStreamTokenVersion - : _resumeToken.version; + // If we have been explicitly instructed to ignore the client's token version, skip this check. + auto version = (clusterTime > _resumeToken.clusterTime || _expCtx->ignoreTokenVersionOnResume) + ? _expCtx->changeStreamTokenVersion + : _resumeToken.version; // Construct and return the final resume token. return {clusterTime, version, txnOpIndex, uuid, operationType, documentKey, opDescription}; diff --git a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp index 2b16d4dacda..78e945a4ae8 100644 --- a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp +++ b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.cpp @@ -37,7 +37,7 @@ #include "mongo/db/catalog/collection.h" #include "mongo/db/change_stream_options_manager.h" #include "mongo/db/client.h" -#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/concurrency/exception_util.h" #include "mongo/db/db_raii.h" #include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/change_stream_preimage_gen.h" @@ -58,18 +58,6 @@ MONGO_FAIL_POINT_DEFINE(changeStreamPreImageRemoverCurrentTime); namespace preImageRemoverInternal { -bool PreImageAttributes::isExpiredPreImage(const boost::optional<Date_t>& preImageExpirationTime, - const Timestamp& earliestOplogEntryTimestamp) { - // Pre-image oplog entry is no longer present in the oplog if its timestamp is smaller - // than the 'earliestOplogEntryTimestamp'. - const bool preImageOplogEntryIsDeleted = ts < earliestOplogEntryTimestamp; - const auto expirationTime = preImageExpirationTime.get_value_or(Date_t::min()); - - // Pre-image is expired if its corresponding oplog entry is deleted or its operation - // time is less than or equal to the expiration time. - return preImageOplogEntryIsDeleted || operationTime <= expirationTime; -} - // Get the 'expireAfterSeconds' from the 'ChangeStreamOptions' if not 'off', boost::none otherwise. boost::optional<std::int64_t> getExpireAfterSecondsFromChangeStreamOptions( ChangeStreamOptions& changeStreamOptions) { @@ -108,18 +96,48 @@ RecordId toRecordId(ChangeStreamPreImageId id) { } /** + * Finds the next collection UUID in the change stream pre-images collection 'preImagesCollPtr' for + * which collection UUID is greater than 'collectionUUID'. Returns boost::none if the next + * collection is not found. + */ +boost::optional<UUID> findNextCollectionUUID(OperationContext* opCtx, + const CollectionPtr* preImagesCollPtr, + boost::optional<UUID> collectionUUID + +) { + BSONObj preImageObj; + auto minRecordId = collectionUUID + ? boost::make_optional(RecordIdBound(toRecordId(ChangeStreamPreImageId( + *collectionUUID, Timestamp::max(), std::numeric_limits<int64_t>::max())))) + : boost::none; + auto planExecutor = + InternalPlanner::collectionScan(opCtx, + preImagesCollPtr, + PlanYieldPolicy::YieldPolicy::INTERRUPT_ONLY, + InternalPlanner::Direction::FORWARD, + boost::none /* resumeAfterRecordId */, + std::move(minRecordId)); + if (planExecutor->getNext(&preImageObj, nullptr) == PlanExecutor::IS_EOF) { + return boost::none; + } + auto parsedUUID = UUID::parse(preImageObj["_id"].Obj()["nsUUID"]); + tassert(7027400, "Pre-image collection UUID must be of UUID type", parsedUUID.isOK()); + return {std::move(parsedUUID.getValue())}; +} + +/** * Scans the 'config.system.preimages' collection and deletes the expired pre-images from it. * * Pre-images are ordered by collection UUID, ie. if UUID of collection A is ordered before UUID of * collection B, then pre-images of collection A will be stored before pre-images of collection B. * - * While scanning the collection for expired pre-images, each pre-image timestamp is compared - * against the 'earliestOplogEntryTimestamp' value. Any pre-image that has a timestamp greater than - * the 'earliestOplogEntryTimestamp' value is not considered for deletion and the cursor seeks to - * the next UUID in the collection. - * - * Seek to the next UUID is done by setting the values of 'Timestamp' and 'ApplyOpsIndex' fields to - * max, ie. (currentPreImage.nsUUID, Timestamp::max(), ApplyOpsIndex::max()). + * Pre-images are considered expired based on expiration parameter. In case when expiration + * parameter is not set a pre-image is considered expired if its timestamp is smaller than the + * timestamp of the earliest oplog entry. In case when expiration parameter is specified, aside from + * timestamp check a check on the wall clock time of the pre-image recording ('operationTime') is + * performed. If the difference between 'currentTimeForTimeBasedExpiration' and 'operationTime' is + * larger than expiration parameter, the pre-image is considered expired. One of those two + * conditions must be true for a pre-image to be eligible for deletion. * * +-------------------------+ * | config.system.preimages | @@ -134,267 +152,96 @@ RecordId toRecordId(ChangeStreamPreImageId id) { * | applyIndex: 0 | | applyIndex: 0 | | applyIndex: 0 | | applyIndex: 1 | * +-------------------+ +-------------------+ +-------------------+ +-------------------+ */ -class ChangeStreamExpiredPreImageIterator { -public: - // Iterator over the range of pre-image documents, where each range defines a set of expired - // pre-image documents of one collection eligible for deletion due to expiration. Lower and - // upper bounds of a range are inclusive. - class Iterator { - public: - using RecordIdRange = std::pair<RecordId, RecordId>; - - Iterator(OperationContext* opCtx, - const CollectionPtr* preImagesCollPtr, - Timestamp earliestOplogEntryTimestamp, - boost::optional<Date_t> preImageExpirationTime, - bool isEndIterator = false) - : _opCtx(opCtx), - _preImagesCollPtr(preImagesCollPtr), - _earliestOplogEntryTimestamp(earliestOplogEntryTimestamp), - _preImageExpirationTime(preImageExpirationTime) { - if (!isEndIterator) { - advance(); - } - } - - const RecordIdRange& operator*() const { - return _currentExpiredPreImageRange; - } - - const RecordIdRange* operator->() const { - return &_currentExpiredPreImageRange; - } - - Iterator& operator++() { - advance(); - return *this; - } - - // Both iterators are equal if they are both pointing to the same expired pre-image range. - friend bool operator==(const Iterator& a, const Iterator& b) { - return a._currentExpiredPreImageRange == b._currentExpiredPreImageRange; - }; - - friend bool operator!=(const Iterator& a, const Iterator& b) { - return !(a == b); - }; - - private: - // Scans the pre-images collection and gets the next expired pre-image range or sets - // '_currentExpiredPreImageRange' to the range with empty record ids in case there are no - // more expired pre-images left. - void advance() { - const auto getNextPreImageAttributes = - [&](std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>& planExecutor) - -> boost::optional<preImageRemoverInternal::PreImageAttributes> { - BSONObj preImageObj; - if (planExecutor->getNext(&preImageObj, nullptr) == PlanExecutor::IS_EOF) { - return boost::none; - } - - auto preImage = - ChangeStreamPreImage::parse(IDLParserErrorContext("pre-image"), preImageObj); - return {{std::move(preImage.getId().getNsUUID()), - std::move(preImage.getId().getTs()), - std::move(preImage.getOperationTime())}}; - }; - - while (true) { - // Fetch the first pre-image from the next collection, that has pre-images enabled. - auto planExecutor = _previousCollectionUUID - ? createCollectionScan(RecordIdBound( - toRecordId(ChangeStreamPreImageId(*_previousCollectionUUID, - Timestamp::max(), - std::numeric_limits<int64_t>::max())))) - : createCollectionScan(boost::none); - auto preImageAttributes = getNextPreImageAttributes(planExecutor); - - // If there aren't any pre-images left, set the range to the empty record ids and - // return. - if (!preImageAttributes) { - _currentExpiredPreImageRange = std::pair(RecordId(), RecordId()); - return; - } - const auto currentCollectionUUID = preImageAttributes->collectionUUID; - _previousCollectionUUID = currentCollectionUUID; - - // If the first pre-image in the current collection is not expired, fetch the first - // pre-image from the next collection. - if (!preImageAttributes->isExpiredPreImage(_preImageExpirationTime, - _earliestOplogEntryTimestamp)) { - continue; - } - - // If an expired pre-image is found, compute the max expired pre-image RecordId for - // this collection depending on the expiration parameter being set. - const auto minKey = - toRecordId(ChangeStreamPreImageId(currentCollectionUUID, Timestamp(), 0)); - RecordId maxKey; - if (_preImageExpirationTime) { - // Reset the collection scan to start one increment before the - // '_earliestOplogEntryTimestamp', as the pre-images with smaller or equal - // timestamp are guaranteed to be expired. - Timestamp lastExpiredPreimageTs(_earliestOplogEntryTimestamp.asULL() - 1); - auto planExecutor = createCollectionScan(RecordIdBound( - toRecordId(ChangeStreamPreImageId(currentCollectionUUID, - lastExpiredPreimageTs, - std::numeric_limits<int64_t>::max())))); - - // Iterate over all the expired pre-images in the collection in order to find - // the max RecordId. - while ((preImageAttributes = getNextPreImageAttributes(planExecutor)) && - preImageAttributes->isExpiredPreImage(_preImageExpirationTime, - _earliestOplogEntryTimestamp) && - preImageAttributes->collectionUUID == currentCollectionUUID) { - lastExpiredPreimageTs = preImageAttributes->ts; - } - - maxKey = - toRecordId(ChangeStreamPreImageId(currentCollectionUUID, - lastExpiredPreimageTs, - std::numeric_limits<int64_t>::max())); - } else { - // If the expiration parameter is not set, then the last expired pre-image - // timestamp equals to one increment before the '_earliestOplogEntryTimestamp'. - maxKey = toRecordId( - ChangeStreamPreImageId(currentCollectionUUID, - Timestamp(_earliestOplogEntryTimestamp.asULL() - 1), - std::numeric_limits<int64_t>::max())); - } - tassert(6138300, - "Max key of the expired pre-image range has to be valid", - maxKey.isValid()); - _currentExpiredPreImageRange = std::pair(minKey, maxKey); - return; - } - } - - // Set up the new collection scan to start from the 'minKey'. - std::unique_ptr<PlanExecutor, PlanExecutor::Deleter> createCollectionScan( - boost::optional<RecordIdBound> minKey) const { - return InternalPlanner::collectionScan(_opCtx, - _preImagesCollPtr, - PlanYieldPolicy::YieldPolicy::INTERRUPT_ONLY, - InternalPlanner::Direction::FORWARD, - boost::none, - minKey); - } - - OperationContext* _opCtx; - const CollectionPtr* _preImagesCollPtr; - RecordIdRange _currentExpiredPreImageRange; - boost::optional<UUID> _previousCollectionUUID; - const Timestamp _earliestOplogEntryTimestamp; - - // The pre-images with operation time less than or equal to the '_preImageExpirationTime' - // are considered expired. - const boost::optional<Date_t> _preImageExpirationTime; - }; - - ChangeStreamExpiredPreImageIterator( - OperationContext* opCtx, - const CollectionPtr* preImagesCollPtr, - const Timestamp earliestOplogEntryTimestamp, - const boost::optional<Date_t> preImageExpirationTime = boost::none) - : _opCtx(opCtx), - _preImagesCollPtr(preImagesCollPtr), - _earliestOplogEntryTimestamp(earliestOplogEntryTimestamp), - _preImageExpirationTime(preImageExpirationTime) {} - - Iterator begin() const { - return Iterator( - _opCtx, _preImagesCollPtr, _earliestOplogEntryTimestamp, _preImageExpirationTime); - } - - Iterator end() const { - return Iterator(_opCtx, - _preImagesCollPtr, - _earliestOplogEntryTimestamp, - _preImageExpirationTime, - true /*isEndIterator*/); - } - -private: - OperationContext* _opCtx; - const CollectionPtr* _preImagesCollPtr; - const Timestamp _earliestOplogEntryTimestamp; - const boost::optional<Date_t> _preImageExpirationTime; -}; - -void deleteExpiredChangeStreamPreImages(Client* client, Date_t currentTimeForTimeBasedExpiration) { - const auto startTime = Date_t::now(); - auto opCtx = client->makeOperationContext(); - +size_t deleteExpiredChangeStreamPreImages(OperationContext* opCtx, + Date_t currentTimeForTimeBasedExpiration) { // Acquire intent-exclusive lock on the pre-images collection. Early exit if the collection // doesn't exist. - AutoGetCollection autoColl( - opCtx.get(), NamespaceString::kChangeStreamPreImagesNamespace, MODE_IX); + AutoGetCollection autoColl(opCtx, NamespaceString::kChangeStreamPreImagesNamespace, MODE_IX); const auto& preImagesColl = autoColl.getCollection(); if (!preImagesColl) { - return; + return 0; } // Do not run the job on secondaries. - if (!repl::ReplicationCoordinator::get(opCtx.get()) - ->canAcceptWritesForDatabase(opCtx.get(), NamespaceString::kAdminDb)) { - return; + if (!repl::ReplicationCoordinator::get(opCtx)->canAcceptWritesForDatabase( + opCtx, NamespaceString::kAdminDb)) { + return 0; } - // Get the timestamp of the ealiest oplog entry. + // Get the timestamp of the earliest oplog entry. const auto currentEarliestOplogEntryTs = - repl::StorageInterface::get(client->getServiceContext()) - ->getEarliestOplogTimestamp(opCtx.get()); + repl::StorageInterface::get(opCtx->getServiceContext())->getEarliestOplogTimestamp(opCtx); const bool isBatchedRemoval = gBatchedExpiredChangeStreamPreImageRemoval.load(); size_t numberOfRemovals = 0; + const auto preImageExpirationTime = ::mongo::preImageRemoverInternal::getPreImageExpirationTime( + opCtx, currentTimeForTimeBasedExpiration); + + // Configure the filter for the case when expiration parameter is set. + OrMatchExpression filter; + const MatchExpression* filterPtr = nullptr; + if (preImageExpirationTime) { + filter.add( + std::make_unique<LTMatchExpression>("_id.ts"_sd, Value(currentEarliestOplogEntryTs))); + filter.add(std::make_unique<LTEMatchExpression>("operationTime"_sd, + Value(*preImageExpirationTime))); + filterPtr = &filter; + } + const bool shouldReturnEofOnFilterMismatch = preImageExpirationTime.has_value(); - ChangeStreamExpiredPreImageIterator expiredPreImages( - opCtx.get(), - &preImagesColl, - currentEarliestOplogEntryTs, - ::mongo::preImageRemoverInternal::getPreImageExpirationTime( - opCtx.get(), currentTimeForTimeBasedExpiration)); - - for (const auto& collectionRange : expiredPreImages) { + boost::optional<UUID> currentCollectionUUID = boost::none; + while ((currentCollectionUUID = + findNextCollectionUUID(opCtx, &preImagesColl, currentCollectionUUID))) { writeConflictRetry( - opCtx.get(), + opCtx, "ChangeStreamExpiredPreImagesRemover", NamespaceString::kChangeStreamPreImagesNamespace.ns(), [&] { auto params = std::make_unique<DeleteStageParams>(); params->isMulti = true; - boost::optional<std::unique_ptr<BatchedDeleteStageBatchParams>> batchParams; + std::unique_ptr<BatchedDeleteStageBatchParams> batchedDeleteParams; if (isBatchedRemoval) { - batchParams = std::make_unique<BatchedDeleteStageBatchParams>(); + batchedDeleteParams = std::make_unique<BatchedDeleteStageBatchParams>(); } + RecordIdBound minRecordId( + toRecordId(ChangeStreamPreImageId(*currentCollectionUUID, Timestamp(), 0))); + + // If the expiration parameter is set, the 'maxRecord' is set to the maximum + // RecordId for this collection. Whether the pre-image has to be deleted will be + // determined by the filtering MatchExpression. + // + // If the expiration parameter is not set, then the last expired pre-image timestamp + // equals to one increment before the 'currentEarliestOplogEntryTs'. + RecordIdBound maxRecordId = RecordIdBound(toRecordId(ChangeStreamPreImageId( + *currentCollectionUUID, + preImageExpirationTime ? Timestamp::max() + : Timestamp(currentEarliestOplogEntryTs.asULL() - 1), + std::numeric_limits<int64_t>::max()))); auto exec = InternalPlanner::deleteWithCollectionScan( - opCtx.get(), + opCtx, &preImagesColl, std::move(params), PlanYieldPolicy::YieldPolicy::YIELD_AUTO, InternalPlanner::Direction::FORWARD, - RecordIdBound(collectionRange.first), - RecordIdBound(collectionRange.second), + std::move(minRecordId), + std::move(maxRecordId), CollectionScanParams::ScanBoundInclusion::kIncludeBothStartAndEndRecords, - std::move(batchParams)); + std::move(batchedDeleteParams), + filterPtr, + shouldReturnEofOnFilterMismatch); numberOfRemovals += exec->executeDelete(); }); } - - if (numberOfRemovals > 0) { - LOGV2_DEBUG(5869104, - 3, - "Periodic expired pre-images removal job finished executing", - "numberOfRemovals"_attr = numberOfRemovals, - "jobDuration"_attr = (Date_t::now() - startTime).toString()); - } + return numberOfRemovals; } void performExpiredChangeStreamPreImagesRemovalPass(Client* client) { + ServiceContext::UniqueOperationContext opCtx; try { Date_t currentTimeForTimeBasedExpiration = Date_t::now(); + opCtx = client->makeOperationContext(); changeStreamPreImageRemoverCurrentTime.execute([&](const BSONObj& data) { // Populate the current time for time based expiration of pre-images. @@ -409,7 +256,7 @@ void performExpiredChangeStreamPreImagesRemovalPass(Client* client) { currentTimeForTimeBasedExpiration = currentTimeElem.Date(); } }); - deleteExpiredChangeStreamPreImages(client, currentTimeForTimeBasedExpiration); + deleteExpiredChangeStreamPreImages(opCtx.get(), currentTimeForTimeBasedExpiration); } catch (const ExceptionForCat<ErrorCategory::Interruption>&) { LOGV2_WARNING(5869105, "Periodic expired pre-images removal job was interrupted"); } catch (const DBException& exception) { diff --git a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h index 0ddd491991f..19315825043 100644 --- a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h +++ b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover.h @@ -35,22 +35,6 @@ namespace mongo { namespace preImageRemoverInternal { -/** - * Specifies attributes that determines if the pre-image has been expired or not. - */ -struct PreImageAttributes { - mongo::UUID collectionUUID; - Timestamp ts; - Date_t operationTime; - - /** - * Determines if the pre-image is considered expired based on the expiration parameter being - * set. - */ - bool isExpiredPreImage(const boost::optional<Date_t>& preImageExpirationTime, - const Timestamp& earliestOplogEntryTimestamp); -}; - boost::optional<Date_t> getPreImageExpirationTime(OperationContext* opCtx, Date_t currentTime); } // namespace preImageRemoverInternal diff --git a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp index ec49c8453d3..b2562a1db44 100644 --- a/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp +++ b/src/mongo/db/pipeline/change_stream_expired_pre_image_remover_test.cpp @@ -61,16 +61,6 @@ public: ASSERT_EQ(changeStreamOptionsManager.setOptions(opCtx, changeStreamOptions).getStatus(), ErrorCodes::OK); } - - bool isExpiredPreImage(const Timestamp& preImageTs, - const Date_t& preImageOperationTime, - const boost::optional<Date_t>& preImageExpirationTime, - const Timestamp& earliestOplogEntryTimestamp) { - preImageRemoverInternal::PreImageAttributes preImageAttributes{ - UUID::gen(), preImageTs, preImageOperationTime}; - return preImageAttributes.isExpiredPreImage(preImageExpirationTime, - earliestOplogEntryTimestamp); - } }; TEST_F(ChangeStreamPreImageExpirationPolicyTest, getPreImageExpirationTimeWithValidIntegralValue) { @@ -107,39 +97,5 @@ TEST_F(ChangeStreamPreImageExpirationPolicyTest, getPreImageExpirationTimeWithOf preImageRemoverInternal::getPreImageExpirationTime(opCtx.get(), currentTime); ASSERT_FALSE(receivedExpireAfterSeconds); } - -TEST_F(ChangeStreamPreImageExpirationPolicyTest, preImageShouldHaveExpiredWithOlderTimestamp) { - ASSERT_TRUE( - isExpiredPreImage(Timestamp(Seconds(100000), 0U) /* preImageTs */, - Date_t::now() /* preImageOperationTime */, - Date_t::now() /* preImageExpirationTime */, - Timestamp(Seconds(100000), 1U)) /* earliestOplogEntryTimestamp */); -} - -TEST_F(ChangeStreamPreImageExpirationPolicyTest, preImageShouldNotHaveExpired) { - ASSERT_FALSE( - isExpiredPreImage(Timestamp(Seconds(100000), 1U) /* preImageTs */, - Date_t::now() + Seconds(1) /* preImageOperationTime */, - Date_t::now() /* preImageExpirationTime */, - Timestamp(Seconds(100000), 0U)) /* earliestOplogEntryTimestamp */); -} - -TEST_F(ChangeStreamPreImageExpirationPolicyTest, preImageShouldHaveExpiredWithOlderOperationTime) { - ASSERT_TRUE( - isExpiredPreImage(Timestamp(Seconds(100000), 1U) /* preImageTs */, - Date_t::now() /* preImageOperationTime */, - Date_t::now() + Seconds(1) /* preImageExpirationTime */, - Timestamp(Seconds(100000), 0U)) /* earliestOplogEntryTimestamp */); -} - -TEST_F(ChangeStreamPreImageExpirationPolicyTest, - preImageShouldNotHaveExpiredWithNullExpirationTime) { - ASSERT_TRUE( - isExpiredPreImage(Timestamp(Seconds(100000), 0U) /* preImageTs */, - Date_t::now() /* preImageOperationTime */, - boost::none /* preImageExpirationTime */, - Timestamp(Seconds(100000), 1U)) /* earliestOplogEntryTimestamp */); -} - } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_helpers.cpp b/src/mongo/db/pipeline/change_stream_helpers.cpp new file mode 100644 index 00000000000..720646d32ea --- /dev/null +++ b/src/mongo/db/pipeline/change_stream_helpers.cpp @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/change_stream_helpers.h" + +#include "mongo/db/pipeline/document_source_change_stream_gen.h" + +namespace mongo { +namespace change_stream { +ResumeTokenData resolveResumeTokenFromSpec(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const DocumentSourceChangeStreamSpec& spec) { + + if (spec.getStartAfter()) { + return spec.getStartAfter()->getData(); + } else if (spec.getResumeAfter()) { + return spec.getResumeAfter()->getData(); + } else if (spec.getStartAtOperationTime()) { + return ResumeToken::makeHighWaterMarkToken(*spec.getStartAtOperationTime(), + expCtx->changeStreamTokenVersion) + .getData(); + } + tasserted(5666901, + "Expected one of 'startAfter', 'resumeAfter' or 'startAtOperationTime' to be " + "populated in $changeStream spec"); +} +} // namespace change_stream +} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_helpers.h b/src/mongo/db/pipeline/change_stream_helpers.h new file mode 100644 index 00000000000..7adc4f92f8f --- /dev/null +++ b/src/mongo/db/pipeline/change_stream_helpers.h @@ -0,0 +1,47 @@ +/** + * Copyright (C) 2023-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/pipeline/document_source_change_stream_gen.h" +#include "mongo/db/pipeline/expression_context.h" + +namespace mongo { + +namespace change_stream { +/** + * Extracts the resume token from the given spec. If a 'startAtOperationTime' is specified, + * returns the equivalent high-watermark token. This method should only ever be called on a spec + * where one of 'resumeAfter', 'startAfter', or 'startAtOperationTime' is populated. + */ +ResumeTokenData resolveResumeTokenFromSpec(const boost::intrusive_ptr<ExpressionContext>& expCtx, + const DocumentSourceChangeStreamSpec& spec); + +} // namespace change_stream +} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp b/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp index ab0ade1e517..f7703354560 100644 --- a/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp +++ b/src/mongo/db/pipeline/change_stream_rewrite_helpers.cpp @@ -29,6 +29,8 @@ #include "mongo/db/pipeline/change_stream_rewrite_helpers.h" +#include <boost/algorithm/string/replace.hpp> + #include "mongo/db/matcher/expression_always_boolean.h" #include "mongo/db/matcher/expression_expr.h" #include "mongo/db/pipeline/document_source_change_stream.h" @@ -888,9 +890,12 @@ std::unique_ptr<MatchExpression> matchRewriteGenericNamespace( }(); // Convert the MatchExpression $regex into a $regexMatch on the corresponding field. + // Backslashes must be escaped to ensure they retain their special behavior. + const auto regex = + boost::replace_all_copy(std::string(nsElem.regex()), R"(\)", R"(\\)"); const std::string exprRegexMatch = str::stream() - << "{$regexMatch: {input: " << exprDbOrCollName << ", regex: '" - << nsElem.regex() << "', options: '" << nsElem.regexFlags() << "'}}"; + << "{$regexMatch: {input: " << exprDbOrCollName << ", regex: '" << regex + << "', options: '" << nsElem.regexFlags() << "'}}"; // Finally, wrap the regex in a $let which defines the '$$oplogField' variable. const std::string exprRewrittenPredicate = str::stream() diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp b/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp new file mode 100644 index 00000000000..19fba5afea5 --- /dev/null +++ b/src/mongo/db/pipeline/change_stream_split_event_helpers.cpp @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2023-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/change_stream_split_event_helpers.h" + +#include "mongo/db/pipeline/field_path.h" +#include "mongo/db/pipeline/resume_token.h" + +namespace mongo { +namespace change_stream_split_event { + +std::pair<Document, size_t> processChangeEventBeforeSplit(const Document& event, + bool withMetadata) { + if (withMetadata) { + auto eventBson = event.toBsonWithMetaData<BSONObj::LargeSizeTrait>(); + return {Document::fromBsonWithMetaData(eventBson), eventBson.objsize()}; + } else { + // Serialize just the user data, and add the metadata fields separately. + auto eventBson = event.toBson<BSONObj::LargeSizeTrait>(); + MutableDocument mutDoc(Document{eventBson}); + mutDoc.copyMetaDataFrom(event); + return {mutDoc.freeze(), eventBson.objsize()}; + } +} + +std::queue<Document> splitChangeEvent(const Document& event, + size_t maxFragmentBsonSize, + size_t skipFirstFragments) { + // Extract the underlying BSON. We expect the event to be trivially convertible either with + // or without metadata, so we attempt to optimize the serialization here. + auto eventBson = + (event.isTriviallyConvertible() ? event.toBson<BSONObj::LargeSizeTrait>() + : event.toBsonWithMetaData<BSONObj::LargeSizeTrait>()); + + // Construct a sorted map of fields ordered by size and key for a deterministic greedy strategy + // to minimize the total number of fragments (the first fragment contains as many fields as + // possible). Don't include the original '_id' field, since each fragment will have its own. + std::map<std::pair<size_t, std::string>, Value> sortedFieldMap; + for (auto it = event.fieldIterator(); it.more();) { + auto&& [key, value] = it.next(); + if (key != kIdField) { + sortedFieldMap.emplace(std::make_pair(eventBson[key].size(), key), value); + } + } + + uassert(7182502, + "Cannot split an empty event or an event containing solely '_id' field", + !sortedFieldMap.empty()); + + auto resumeTokenData = + ResumeToken::parse(event.metadata().getSortKey().getDocument()).getData(); + + std::list<MutableDocument> fragments; + for (auto it = sortedFieldMap.cbegin(); it != sortedFieldMap.cend();) { + // Update the resume token with the index of the fragment we're about to add. + resumeTokenData.fragmentNum = fragments.size(); + + // Add a new fragment at the end of the fragments list. + auto& fragment = fragments.emplace_back(); + + // Add fields required by all fragments. + ResumeToken token(resumeTokenData); + fragment.metadata().setSortKey(Value(token.toDocument()), true); + fragment.addField(kIdField, fragment.metadata().getSortKey()); + fragment.addField(kSplitEventField, + Value(Document{{kFragmentNumberField, static_cast<int>(fragments.size())}, + {kTotalFragmentsField, 0}})); + + auto fragmentBsonSize = static_cast<size_t>(fragment.peek().toBsonWithMetaData().objsize()); + + // Fill the fragment with as many fields as we can until we run out or exceed max size. + // Always make sure we add at least one new field on each iteration. + do { + fragment.addField(it->first.second /* field name */, it->second /* field value */); + fragmentBsonSize += it->first.first /* field size */; + } while (++it != sortedFieldMap.cend() && + fragmentBsonSize + it->first.first /* field size */ <= maxFragmentBsonSize); + + uassert(7182500, + str::stream() << "Splitting change event failed: fragment size " << fragmentBsonSize + << " is greater than maximum allowed fragment size " + << maxFragmentBsonSize, + fragmentBsonSize <= maxFragmentBsonSize); + } + + // Iterate over the fragments to populate the 'kTotalFragmentsField' field and freeze the final + // events. + const auto totalFragments = Value(static_cast<int>(fragments.size())); + const auto totalFragmentsFieldPath = + FieldPath::getFullyQualifiedPath(kSplitEventField, kTotalFragmentsField); + + std::queue<Document> outputFragments; + for (auto [it, i] = std::make_pair(fragments.begin(), 0ULL); it != fragments.end(); ++it, ++i) { + // Do not insert first 'skipFirstFragments' into the output. + if (i >= skipFirstFragments) { + it->setNestedField(totalFragmentsFieldPath, totalFragments); + outputFragments.push(it->freeze()); + } + } + + return outputFragments; +} + +} // namespace change_stream_split_event +} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers.h b/src/mongo/db/pipeline/change_stream_split_event_helpers.h new file mode 100644 index 00000000000..ee39b52dead --- /dev/null +++ b/src/mongo/db/pipeline/change_stream_split_event_helpers.h @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2023-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <queue> + +#include "mongo/db/exec/document_value/document.h" + +namespace mongo { +namespace change_stream_split_event { + +constexpr auto kIdField = "_id"_sd; +constexpr auto kSplitEventField = "splitEvent"_sd; +constexpr auto kFragmentNumberField = "fragment"_sd; +constexpr auto kTotalFragmentsField = "of"_sd; + +/** + * Calculates BSON size by serializing the event to BSON. Ensures that the serialization is + * re-usable. The parameter 'withMetadata' desides whether the metadata is counted. + * Also returns a new document optimized for later serialization by PlanExecutorPipeline. + */ +std::pair<Document, size_t> processChangeEventBeforeSplit(const Document& event, bool withMetadata); + +/** + * Splits the given change stream 'event' to several sub-events, called fragments. The size of BSON + * serialization of each fragment does not exceed the given maximum fragment size. Each fragment + * carries additionally fragment's ordinal number and the total number of fragments. Each fragment + * has its own resume token as its '_id' and the sort key. In the resume scenario, the + * 'skipFirstFragments' parameter indicates how many fragments were already received by the client + * and can be skipped. For example, the following change event + * {_id: "RESUMETOKEN1", fullDocument: ..., fullDocumentBeforeChange: ..., ...} + * can be split into the following fragments + * {_id: "RESUMETOKEN2", splitEvent{fragment: 1, of: 2}, fullDocumentBeforeChange: ...} + * {_id: "RESUMETOKEN3", splitEvent{fragment: 2, of: 2}, fullDocument: ...} + */ +std::queue<Document> splitChangeEvent(const Document& event, + size_t maxFragmentBsonSize, + size_t skipFirstFragments = 0); + +} // namespace change_stream_split_event +} // namespace mongo diff --git a/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp b/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp new file mode 100644 index 00000000000..aea1ca6fce8 --- /dev/null +++ b/src/mongo/db/pipeline/change_stream_split_event_helpers_test.cpp @@ -0,0 +1,158 @@ +/** + * Copyright (C) 2023-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/change_stream_split_event_helpers.h" +#include "mongo/db/pipeline/field_path.h" +#include "mongo/db/pipeline/resume_token.h" +#include "mongo/unittest/unittest.h" + +namespace mongo { +namespace { + +using namespace change_stream_split_event; + +class ChangeStreamSplitEventHelpersTest : public unittest::Test { +public: + ChangeStreamSplitEventHelpersTest() { + ResumeTokenData tokenData( + Timestamp(1000, 1), 2, 0, UUID::gen(), Value(Document{{kIdField, 1}})); + doc.metadata().setSortKey(Value(ResumeToken(tokenData).toDocument()), true); + + // Approximate the size of the fragment with no data. + // This size may vary because of the variable serialization of the token - fragments with + // 'splitEvent.fragment' == 1 are 4 bytes shorter, because the token with + // 'tokenData.fragmentNum' == 0 are 2 bytes shorter. + MutableDocument fragment; + tokenData.fragmentNum = 1UL; + fragment.metadata().setSortKey(Value(ResumeToken(tokenData).toDocument()), true); + fragment.addField(kIdField, fragment.metadata().getSortKey()); + fragment.addField(kSplitEventField, + Value(Document{{kFragmentNumberField, 1}, {kTotalFragmentsField, 1}})); + minFragmentSize = static_cast<size_t>(fragment.peek().toBsonWithMetaData().objsize()); + } + + size_t getFieldBsonSize(const Document& doc, const StringData& key) { + return static_cast<size_t>(doc.toBson<BSONObj::LargeSizeTrait>().getField(key).size()); + } + + MutableDocument doc; + size_t minFragmentSize; + FieldPath fragmentNumberPath = + FieldPath::getFullyQualifiedPath(kSplitEventField, kFragmentNumberField); + FieldPath totalFragmentsPath = + FieldPath::getFullyQualifiedPath(kSplitEventField, kTotalFragmentsField); +}; + +TEST_F(ChangeStreamSplitEventHelpersTest, EmptyDocThrows) { + ASSERT_THROWS_CODE(splitChangeEvent(doc.freeze(), minFragmentSize, 0), DBException, 7182502); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, DocWithSolelyIdThrows) { + doc.addField("_id", Value(1)); + ASSERT_THROWS_CODE(splitChangeEvent(doc.freeze(), minFragmentSize, 0), DBException, 7182502); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, BasicSplitWithSingleFragment) { + doc.addField("a", Value(123)); + doc.addField("b", Value(321)); + auto fieldSize = getFieldBsonSize(doc.peek(), "a"); + auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize + fieldSize, 0); + ASSERT_EQ(1UL, fragments.size()); + auto& fragment = fragments.front(); + ASSERT_EQ(1, fragment.getNestedField(fragmentNumberPath).getInt()); + ASSERT_EQ(1, fragment.getNestedField(totalFragmentsPath).getInt()); + ASSERT_EQ(123, fragment.getField("a").getInt()); + ASSERT_EQ(321, fragment.getField("b").getInt()); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, ReplacesIdWithFragmentResumeToken) { + // Replace the test doc's _id with a numeric value. This will be overwritten by the fragment's + // resume token when we split the event. + doc.addField("_id", Value(1)); + doc.addField("a", Value(123)); + auto fieldSize = getFieldBsonSize(doc.peek(), "a"); + auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize, 0); + ASSERT_EQ(1ULL, fragments.size()); + auto& fragment = fragments.front(); + ASSERT_EQ(123, fragment.getField("a").getInt()); + auto tokenData = ResumeToken::parse(fragment.getField(kIdField).getDocument()).getData(); + ASSERT_EQ(0ULL, *tokenData.fragmentNum); + ASSERT_EQ(tokenData, + ResumeToken::parse(fragment.metadata().getSortKey().getDocument()).getData()); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, OversizedFragmentThrows) { + doc.addField("a", Value("very_long_string"_sd)); + auto fieldSize = getFieldBsonSize(doc.peek(), "a"); + ASSERT_THROWS_CODE( + splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize - 5, 0), DBException, 7182500); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, SplitEventAtMaxSizeBoundary) { + // Add two fields of equal size. The first fragment will contain the field with the name + // preceeding in the lexicographic order. + doc.addField("b", Value(321)); + doc.addField("a", Value(123)); + auto fieldSize = getFieldBsonSize(doc.peek(), "b"); + auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize, 0); + ASSERT_EQ(2ULL, fragments.size()); + auto &fragment1 = fragments.front(), fragment2 = fragments.back(); + ASSERT_EQ(123, fragment1.getField("a").getInt()); + ASSERT_EQ(321, fragment2.getField("b").getInt()); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, SplitEventFieldsOrderedInAscendingSize) { + doc.addField("a", Value("unittesting"_sd)); + doc.addField("b", Value("hello"_sd)); + auto fieldSize = getFieldBsonSize(doc.peek(), "a"); + auto fragments = splitChangeEvent(doc.freeze(), minFragmentSize + fieldSize, 0); + ASSERT_EQ(2ULL, fragments.size()); + auto &fragment1 = fragments.front(), fragment2 = fragments.back(); + ASSERT_EQ(1, fragment1.getNestedField(fragmentNumberPath).getInt()); + ASSERT_EQ(2, fragment1.getNestedField(totalFragmentsPath).getInt()); + ASSERT_EQ("hello", fragment1.getField("b").getString()); + ASSERT_EQ(2, fragment2.getNestedField(fragmentNumberPath).getInt()); + ASSERT_EQ(2, fragment2.getNestedField(totalFragmentsPath).getInt()); + ASSERT_EQ("unittesting", fragment2.getField("a").getString()); +} + +TEST_F(ChangeStreamSplitEventHelpersTest, CanSkipFirstNFragments) { + doc.addField("a", Value("unittesting"_sd)); + doc.addField("b", Value("hello"_sd)); + auto fieldSize = getFieldBsonSize(doc.peek(), "a"); + auto fragmentsSkip1 = splitChangeEvent(doc.peek(), minFragmentSize + fieldSize, 1); + ASSERT_EQ(1ULL, fragmentsSkip1.size()); + ASSERT_EQ(2, fragmentsSkip1.front().getNestedField(fragmentNumberPath).getInt()); + ASSERT_EQ(2, fragmentsSkip1.front().getNestedField(totalFragmentsPath).getInt()); + auto fragmentsSkip2 = splitChangeEvent(doc.peek(), minFragmentSize + fieldSize, 2); + ASSERT_EQ(0ULL, fragmentsSkip2.size()); +} + +} // namespace +} // namespace mongo diff --git a/src/mongo/db/pipeline/dependencies.cpp b/src/mongo/db/pipeline/dependencies.cpp index d2a5563c7c7..4480ec8cf6b 100644 --- a/src/mongo/db/pipeline/dependencies.cpp +++ b/src/mongo/db/pipeline/dependencies.cpp @@ -37,11 +37,32 @@ namespace mongo { -std::list<std::string> DepsTracker::sortedFields() const { - // Use a special comparator to put parent fieldpaths before their children. - std::list<std::string> sortedFields(fields.begin(), fields.end()); - sortedFields.sort(PathPrefixComparator()); - return sortedFields; +OrderedPathSet DepsTracker::simplifyDependencies(OrderedPathSet dependencies, + TruncateToRootLevel truncateToRootLevel) { + // The key operation here is folding dependencies into ancestor dependencies, wherever possible. + // This is assisted by a special sort in OrderedPathSet that treats '.' + // as the first char and thus places parent paths directly before their children. + OrderedPathSet returnSet; + std::string last; + for (const auto& path : dependencies) { + if (!last.empty() && str::startsWith(path, last)) { + // We are including a parent of this field, so we can skip this field. + continue; + } + + // Check that the field requested is a valid field name in the agg language. This + // constructor will throw if it isn't. + FieldPath fp(path); + + if (truncateToRootLevel == TruncateToRootLevel::yes) { + last = fp.front().toString() + '.'; + returnSet.insert(fp.front().toString()); + } else { + last = path + '.'; + returnSet.insert(path); + } + } + return returnSet; } BSONObj DepsTracker::toProjectionWithoutMetadata( @@ -59,35 +80,16 @@ BSONObj DepsTracker::toProjectionWithoutMetadata( return bb.obj(); } - // Go through dependency fieldpaths to find the minimal set of projections that cover the - // dependencies. For example, the dependencies ["a.b", "a.b.c.g", "c", "c.d", "f"] would be - // minimally covered by the projection {"a.b": 1, "c": 1, "f": 1}. The key operation here is - // folding dependencies into ancestor dependencies, wherever possible. This is assisted by a - // special sort in DepsTracker::sortedFields that treats '.' as the first char and thus places - // parent paths directly before their children. + // Create a projection from the simplified dependencies (absorbing descendants into parents). + // For example, the dependencies ["a.b", "a.b.c.g", "c", "c.d", "f"] would be + // minimally covered by the projection {"a.b": 1, "c": 1, "f": 1}. bool idSpecified = false; - std::string last; - for (const auto& field : sortedFields()) { - if (str::startsWith(field, "_id") && (field.size() == 3 || field[3] == '.')) { + for (auto path : simplifyDependencies(fields, truncationBehavior)) { + // Remember if _id was specified. If not, we'll later explicitly add {_id: 0} + if (str::startsWith(path, "_id") && (path.size() == 3 || path[3] == '.')) { idSpecified = true; } - - if (!last.empty() && str::startsWith(field, last)) { - // We are including a parent of this field, so we can skip this field. - continue; - } - - // Check that the field requested is a valid field name in the agg language. This - // constructor will throw if it isn't. - FieldPath fp(field); - - if (truncationBehavior == TruncateToRootLevel::yes) { - last = fp.front().toString() + '.'; - bb.append(fp.front(), 1); - } else { - last = field + '.'; - bb.append(field, 1); - } + bb.append(path, 1); } if (!idSpecified) { @@ -109,7 +111,7 @@ void DepsTracker::setNeedsMetadata(DocumentMetadataFields::MetaType type, bool r } // Returns true if the lhs value should sort before the rhs, false otherwise. -bool PathPrefixComparator::operator()(const std::string& lhs, const std::string& rhs) const { +bool PathComparator::operator()(const std::string& lhs, const std::string& rhs) const { constexpr char dot = '.'; for (size_t pos = 0, len = std::min(lhs.size(), rhs.size()); pos < len; ++pos) { diff --git a/src/mongo/db/pipeline/dependencies.h b/src/mongo/db/pipeline/dependencies.h index 3c892de8181..963584ba148 100644 --- a/src/mongo/db/pipeline/dependencies.h +++ b/src/mongo/db/pipeline/dependencies.h @@ -39,6 +39,21 @@ namespace mongo { /** + * Custom comparator that orders fieldpath strings by path prefix first, then by field. + * This ensures that a parent field is ordered directly before its children. + */ +struct PathComparator { + /* Returns true if the lhs value should sort before the rhs, false otherwise. */ + bool operator()(const std::string& lhs, const std::string& rhs) const; +}; + +/** + * Set of field paths strings. When iterated over, a parent path is seen directly before its + * children (or descendants, more generally). Eg., "a", "a.a", "a.b", "a-plus", "b". + */ +typedef std::set<std::string, PathComparator> OrderedPathSet; + +/** * This struct allows components in an agg pipeline to report what they need from their input. */ struct DepsTracker { @@ -104,6 +119,16 @@ struct DepsTracker { enum class TruncateToRootLevel : bool { no, yes }; /** + * Return the set of dependencies with descendant paths removed. + * For example ["a.b", "a.b.f", "c"] --> ["a.b", "c"]. + * + * TruncateToRootLevel::yes requires all dependencies to be top-level. + * The example above would return ["a", "c"] + */ + static OrderedPathSet simplifyDependencies(OrderedPathSet dependencies, + TruncateToRootLevel truncation); + + /** * Returns a projection object covering the non-metadata dependencies tracked by this class, or * empty BSONObj if the entire document is required. By default, the resulting project will * include the full, dotted field names of the dependencies. If 'truncationBehavior' is set to @@ -185,11 +210,11 @@ struct DepsTracker { } /** - * Return fieldpaths ordered such that a parent is immediately before its children. + * Return names of needed fields in dotted notation. A custom comparator orders the fields + * such that a parent is immediately before its children. */ - std::list<std::string> sortedFields() const; + OrderedPathSet fields; - std::set<std::string> fields; // Names of needed fields in dotted notation. std::set<Variables::Id> vars; // IDs of referenced variables. bool needWholeDocument = false; // If true, ignore 'fields'; the whole document is needed. @@ -207,12 +232,4 @@ private: QueryMetadataBitSet _metadataDeps; }; - -/** Custom comparator that orders fieldpath strings by path prefix first, then by field. - * This ensures that a parent field is ordered directly before its children. - */ -struct PathPrefixComparator { - /* Returns true if the lhs value should sort before the rhs, false otherwise. */ - bool operator()(const std::string& lhs, const std::string& rhs) const; -}; } // namespace mongo diff --git a/src/mongo/db/pipeline/dependencies_test.cpp b/src/mongo/db/pipeline/dependencies_test.cpp index 938130b91bd..75451b258b8 100644 --- a/src/mongo/db/pipeline/dependencies_test.cpp +++ b/src/mongo/db/pipeline/dependencies_test.cpp @@ -45,8 +45,8 @@ using std::set; using std::string; template <size_t ArrayLen> -set<string> arrayToSet(const char* (&array)[ArrayLen]) { - set<string> out; +OrderedPathSet arrayToSet(const char* (&array)[ArrayLen]) { + OrderedPathSet out; for (size_t i = 0; i < ArrayLen; i++) out.insert(array[i]); return out; @@ -306,19 +306,17 @@ TEST(DependenciesToProjectionTest, SortFieldPaths) { "b.a" "b.aa" "b.🌲d"}; - DepsTracker deps; - deps.fields = arrayToSet(array); + auto fields = arrayToSet(array); // our custom sort will restore the ordering above - std::list<std::string> fieldPathSorted = deps.sortedFields(); - auto itr = fieldPathSorted.begin(); - for (unsigned long i = 0; i < fieldPathSorted.size(); i++) { + auto itr = fields.begin(); + for (unsigned long i = 0; i < fields.size(); i++) { ASSERT_EQ(*itr, array[i]); ++itr; } } TEST(DependenciesToProjectionTest, PathLessThan) { - auto lessThan = PathPrefixComparator(); + auto lessThan = PathComparator(); ASSERT_FALSE(lessThan("a", "a")); ASSERT_TRUE(lessThan("a", "aa")); ASSERT_TRUE(lessThan("a", "b")); diff --git a/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp b/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp index 069a7e2f0b2..ac8924a13ed 100644 --- a/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp +++ b/src/mongo/db/pipeline/dispatch_shard_pipeline_test.cpp @@ -30,7 +30,7 @@ #include "mongo/db/pipeline/aggregation_request_helper.h" #include "mongo/db/pipeline/sharded_agg_helpers.h" #include "mongo/s/query/sharded_agg_test_fixture.h" -#include "mongo/s/router.h" +#include "mongo/s/router_role.h" namespace mongo { namespace { @@ -53,10 +53,14 @@ TEST_F(DispatchShardPipelineTest, DoesNotSplitPipelineIfTargetingOneShard) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; + const bool startsWithDocuments = false; auto future = launchAsync([&] { - auto results = sharded_agg_helpers::dispatchShardPipeline( - serializedCommand, hasChangeStream, std::move(pipeline)); + auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand, + hasChangeStream, + startsWithDocuments, + std::move(pipeline), + boost::none /*explain*/); ASSERT_EQ(results.remoteCursors.size(), 1UL); ASSERT(!results.splitPipeline); }); @@ -84,10 +88,14 @@ TEST_F(DispatchShardPipelineTest, DoesSplitPipelineIfMatchSpansTwoShards) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; + const bool startsWithDocuments = false; auto future = launchAsync([&] { - auto results = sharded_agg_helpers::dispatchShardPipeline( - serializedCommand, hasChangeStream, std::move(pipeline)); + auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand, + hasChangeStream, + startsWithDocuments, + std::move(pipeline), + boost::none /*explain*/); ASSERT_EQ(results.remoteCursors.size(), 2UL); ASSERT(bool(results.splitPipeline)); }); @@ -118,10 +126,14 @@ TEST_F(DispatchShardPipelineTest, DispatchShardPipelineRetriesOnNetworkError) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; + const bool startsWithDocuments = false; auto future = launchAsync([&] { // Shouldn't throw. - auto results = sharded_agg_helpers::dispatchShardPipeline( - serializedCommand, hasChangeStream, std::move(pipeline)); + auto results = sharded_agg_helpers::dispatchShardPipeline(serializedCommand, + hasChangeStream, + startsWithDocuments, + std::move(pipeline), + boost::none /*explain*/); ASSERT_EQ(results.remoteCursors.size(), 2UL); ASSERT(bool(results.splitPipeline)); }); @@ -163,9 +175,14 @@ TEST_F(DispatchShardPipelineTest, DispatchShardPipelineDoesNotRetryOnStaleConfig const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; + const bool startsWithDocuments = false; + auto future = launchAsync([&] { - ASSERT_THROWS_CODE(sharded_agg_helpers::dispatchShardPipeline( - serializedCommand, hasChangeStream, std::move(pipeline)), + ASSERT_THROWS_CODE(sharded_agg_helpers::dispatchShardPipeline(serializedCommand, + hasChangeStream, + startsWithDocuments, + std::move(pipeline), + boost::none /*explain*/), AssertionException, ErrorCodes::StaleConfig); }); @@ -197,6 +214,7 @@ TEST_F(DispatchShardPipelineTest, WrappedDispatchDoesRetryOnStaleConfigError) { const Document serializedCommand = aggregation_request_helper::serializeToCommandDoc( AggregateCommandRequest(expCtx()->ns, stages)); const bool hasChangeStream = false; + const bool startsWithDocuments = false; auto future = launchAsync([&] { // Shouldn't throw. sharding::router::CollectionRouter router(getServiceContext(), kTestAggregateNss); @@ -204,7 +222,11 @@ TEST_F(DispatchShardPipelineTest, WrappedDispatchDoesRetryOnStaleConfigError) { "dispatch shard pipeline"_sd, [&](OperationContext* opCtx, const ChunkManager& cm) { return sharded_agg_helpers::dispatchShardPipeline( - serializedCommand, hasChangeStream, pipeline->clone()); + serializedCommand, + hasChangeStream, + startsWithDocuments, + pipeline->clone(), + boost::none /*explain*/); }); ASSERT_EQ(results.remoteCursors.size(), 1UL); ASSERT(!bool(results.splitPipeline)); diff --git a/src/mongo/db/pipeline/document_path_support.cpp b/src/mongo/db/pipeline/document_path_support.cpp index a57d0496a1d..eeb0831b55d 100644 --- a/src/mongo/db/pipeline/document_path_support.cpp +++ b/src/mongo/db/pipeline/document_path_support.cpp @@ -136,19 +136,18 @@ StatusWith<Value> extractElementAlongNonArrayPath(const Document& doc, const Fie return curValue; } -BSONObj documentToBsonWithPaths(const Document& input, const std::set<std::string>& paths) { - BSONObjBuilder outputBuilder; +void documentToBsonWithPaths(const Document& input, + const OrderedPathSet& paths, + BSONObjBuilder* builder) { for (auto&& path : paths) { // getNestedField does not handle dotted paths correctly, so instead of retrieving the // entire path, we just extract the first element of the path. const auto prefix = FieldPath::extractFirstFieldFromDottedPath(path); - if (!outputBuilder.hasField(prefix)) { + if (!builder->hasField(prefix)) { // Avoid adding the same prefix twice. - input.getField(prefix).addToBsonObj(&outputBuilder, prefix); + input.getField(prefix).addToBsonObj(builder, prefix); } } - - return outputBuilder.obj(); } } // namespace document_path_support diff --git a/src/mongo/db/pipeline/document_path_support.h b/src/mongo/db/pipeline/document_path_support.h index 5d9f0a1cb6b..b1c127af0e4 100644 --- a/src/mongo/db/pipeline/document_path_support.h +++ b/src/mongo/db/pipeline/document_path_support.h @@ -63,7 +63,14 @@ StatusWith<Value> extractElementAlongNonArrayPath(const Document& doc, const Fie /** * Extracts 'paths' from the input document and returns a BSON object containing only those paths. */ -BSONObj documentToBsonWithPaths(const Document&, const std::set<std::string>& paths); +void documentToBsonWithPaths(const Document&, const OrderedPathSet& paths, BSONObjBuilder* builder); + +template <typename BSONTraits = BSONObj::DefaultSizeTrait> +BSONObj documentToBsonWithPaths(const Document& input, const OrderedPathSet& paths) { + BSONObjBuilder outputBuilder; + documentToBsonWithPaths(input, paths, &outputBuilder); + return outputBuilder.obj<BSONTraits>(); +} /** * Extracts 'paths' from the input document to a flat document. diff --git a/src/mongo/db/pipeline/document_path_support_test.cpp b/src/mongo/db/pipeline/document_path_support_test.cpp index 5e966e08a75..3df55d54e90 100644 --- a/src/mongo/db/pipeline/document_path_support_test.cpp +++ b/src/mongo/db/pipeline/document_path_support_test.cpp @@ -42,6 +42,15 @@ #include "mongo/db/pipeline/field_path.h" #include "mongo/unittest/unittest.h" +#define ASSERT_DOES_NOT_THROW(EXPRESSION) \ + try { \ + EXPRESSION; \ + } catch (const AssertionException& e) { \ + str::stream err; \ + err << "Threw an exception incorrectly: " << e.toString(); \ + ::mongo::unittest::TestAssertionFailure(__FILE__, __LINE__, err).stream(); \ + } + namespace mongo { namespace document_path_support { @@ -351,8 +360,8 @@ TEST(DocumentToBsonWithPathsTest, MissingFieldShouldNotAppearInResult) { TEST(DocumentToBsonWithPathsTest, ShouldSerializeNothingIfNothingIsNeeded) { Document input(fromjson("{a: 1, b: {c: 1}}")); BSONObj expected; - ASSERT_BSONOBJ_EQ( - expected, document_path_support::documentToBsonWithPaths(input, std::set<std::string>{})); + ASSERT_BSONOBJ_EQ(expected, + document_path_support::documentToBsonWithPaths(input, OrderedPathSet{})); } TEST(DocumentToBsonWithPathsTest, ShouldExtractEntireArrayFromPrefixOfDottedField) { @@ -361,6 +370,26 @@ TEST(DocumentToBsonWithPathsTest, ShouldExtractEntireArrayFromPrefixOfDottedFiel ASSERT_BSONOBJ_EQ(expected, document_path_support::documentToBsonWithPaths(input, {"a.b"})); } +TEST(DocumentToBsonWithPathsTest, SizeTraits) { + constexpr size_t longStringLength = 9 * 1024 * 1024; + static_assert(longStringLength <= BSONObjMaxInternalSize && + 2 * longStringLength > BSONObjMaxInternalSize && + 2 * longStringLength <= BufferMaxSize); + std::string longString(longStringLength, 'A'); + MutableDocument md; + md.addField("a", Value(longString)); + md.addField("b", Value(longString)); + ASSERT_DOES_NOT_THROW(document_path_support::documentToBsonWithPaths(md.peek(), {"a"})); + ASSERT_THROWS_CODE(document_path_support::documentToBsonWithPaths(md.peek(), {"a", "b"}), + DBException, + ErrorCodes::BSONObjectTooLarge); + ASSERT_THROWS_CODE(document_path_support::documentToBsonWithPaths<BSONObj::DefaultSizeTrait>( + md.peek(), {"a", "b"}), + DBException, + ErrorCodes::BSONObjectTooLarge); + ASSERT_DOES_NOT_THROW(document_path_support::documentToBsonWithPaths<BSONObj::LargeSizeTrait>( + md.peek(), {"a", "b"})); +} } // namespace } // namespace document_path_support } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source.h b/src/mongo/db/pipeline/document_source.h index ee3977aed18..a7188df709a 100644 --- a/src/mongo/db/pipeline/document_source.h +++ b/src/mongo/db/pipeline/document_source.h @@ -381,6 +381,16 @@ public: Pipeline::SplitState = Pipeline::SplitState::kUnsplit) const = 0; /** + * If a stage's StageConstraints::PositionRequirement is kCustom, then it should also override + * this method, which will be called by the validation process. + */ + virtual void validatePipelinePosition(bool alreadyOptimized, + Pipeline::SourceContainer::const_iterator pos, + const Pipeline::SourceContainer& container) const { + MONGO_UNIMPLEMENTED_TASSERT(7183905); + }; + + /** * Informs the stage that it is no longer needed and can release its resources. After dispose() * is called the stage must still be able to handle calls to getNext(), but can return kEOF. * @@ -601,9 +611,7 @@ public: kAllExcept, }; - GetModPathsReturn(Type type, - std::set<std::string>&& paths, - StringMap<std::string>&& renames) + GetModPathsReturn(Type type, OrderedPathSet&& paths, StringMap<std::string>&& renames) : type(type), paths(std::move(paths)), renames(std::move(renames)) {} std::set<std::string> getNewNames() { @@ -649,11 +657,11 @@ public: return true; } // Cannot hit. - MONGO_UNREACHABLE_TASSERT(6434901); + MONGO_UNREACHABLE_TASSERT(6434902); } Type type; - std::set<std::string> paths; + OrderedPathSet paths; // Stages may fill out 'renames' to contain information about path renames. Each entry in // 'renames' maps from the new name of the path (valid in documents flowing *out* of this @@ -676,7 +684,7 @@ public: * See GetModPathsReturn above for the possible return values and what they mean. */ virtual GetModPathsReturn getModifiedPaths() const { - return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet{}, {}}; } /** @@ -714,7 +722,7 @@ public: * parallel since it will preserve the shard key. */ virtual bool canRunInParallelBeforeWriteStage( - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const { + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const { return false; } diff --git a/src/mongo/db/pipeline/document_source_bucket_auto.cpp b/src/mongo/db/pipeline/document_source_bucket_auto.cpp index 10300e9c334..b0349125a98 100644 --- a/src/mongo/db/pipeline/document_source_bucket_auto.cpp +++ b/src/mongo/db/pipeline/document_source_bucket_auto.cpp @@ -221,7 +221,7 @@ void DocumentSourceBucketAuto::initializeBucketIteration() { auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx); metricsCollector.incrementKeysSorted(_sorter->numSorted()); - metricsCollector.incrementSorterSpills(_sorter->numSpills()); + metricsCollector.incrementSorterSpills(_sorter->stats().spilledRanges()); _sorter.reset(); diff --git a/src/mongo/db/pipeline/document_source_change_stream.cpp b/src/mongo/db/pipeline/document_source_change_stream.cpp index 4fc0041cc0e..3f326a1959f 100644 --- a/src/mongo/db/pipeline/document_source_change_stream.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream.cpp @@ -37,6 +37,7 @@ #include "mongo/db/pipeline/aggregate_command_gen.h" #include "mongo/db/pipeline/change_stream_constants.h" #include "mongo/db/pipeline/change_stream_filter_helpers.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/change_stream_helpers_legacy.h" #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/document_source_change_stream_add_post_image.h" @@ -47,6 +48,7 @@ #include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" #include "mongo/db/pipeline/document_source_change_stream_handle_topology_change.h" #include "mongo/db/pipeline/document_source_change_stream_oplog_match.h" +#include "mongo/db/pipeline/document_source_change_stream_split_large_event.h" #include "mongo/db/pipeline/document_source_change_stream_transform.h" #include "mongo/db/pipeline/document_source_change_stream_unwind_transaction.h" #include "mongo/db/pipeline/document_source_limit.h" @@ -226,23 +228,6 @@ std::string DocumentSourceChangeStream::regexEscapeNsForChangeStream(StringData return result; } -ResumeTokenData DocumentSourceChangeStream::resolveResumeTokenFromSpec( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const DocumentSourceChangeStreamSpec& spec) { - if (spec.getStartAfter()) { - return spec.getStartAfter()->getData(); - } else if (spec.getResumeAfter()) { - return spec.getResumeAfter()->getData(); - } else if (spec.getStartAtOperationTime()) { - return ResumeToken::makeHighWaterMarkToken(*spec.getStartAtOperationTime(), - expCtx->changeStreamTokenVersion) - .getData(); - } - tasserted(5666901, - "Expected one of 'startAfter', 'resumeAfter' or 'startAtOperationTime' to be " - "populated in $changeStream spec"); -} - Timestamp DocumentSourceChangeStream::getStartTimeForNewStream( const boost::intrusive_ptr<ExpressionContext>& expCtx) { // If we do not have an explicit starting point, we should start from the latest majority @@ -272,6 +257,13 @@ list<intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::createFromBson( // Make sure that it is legal to run this $changeStream before proceeding. DocumentSourceChangeStream::assertIsLegalSpecification(expCtx, spec); + // If the user did not specify an explicit starting point, set it to the current time. + if (!spec.getResumeAfter() && !spec.getStartAfter() && !spec.getStartAtOperationTime()) { + // Make sure we update the 'startAtOperationTime' in the 'spec' so that we serialize the + // correct start point when sending it to the shards. + spec.setStartAtOperationTime(DocumentSourceChangeStream::getStartTimeForNewStream(expCtx)); + } + // Save a copy of the spec on the expression context. Used when building the oplog filter. expCtx->changeStreamSpec = spec; @@ -287,15 +279,8 @@ std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::_bui const boost::intrusive_ptr<ExpressionContext>& expCtx, DocumentSourceChangeStreamSpec spec) { std::list<boost::intrusive_ptr<DocumentSource>> stages; - // If the user did not specify an explicit starting point, set it to the current time. - if (!spec.getResumeAfter() && !spec.getStartAfter() && !spec.getStartAtOperationTime()) { - // Make sure we update the 'startAtOperationTime' in the 'spec' so that we serialize the - // correct start point when sending it to the shards. - spec.setStartAtOperationTime(DocumentSourceChangeStream::getStartTimeForNewStream(expCtx)); - } - // Obtain the resume token from the spec. This will be used when building the pipeline. - auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); // Unfold the $changeStream into its constituent stages and add them to the pipeline. stages.push_back(DocumentSourceChangeStreamOplogMatch::create(expCtx, spec)); @@ -310,11 +295,9 @@ std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::_bui // whether the event that matches the resume token should be followed by an "invalidate" event. stages.push_back(DocumentSourceChangeStreamCheckInvalidate::create(expCtx, spec)); - // If the starting point is a high water mark, or if we will be splitting the pipeline for - // dispatch to the shards in a cluster, we must include a DSCSCheckResumability stage. - if (expCtx->inMongos || ResumeToken::isHighWaterMarkToken(resumeToken)) { - stages.push_back(DocumentSourceChangeStreamCheckResumability::create(expCtx, spec)); - } + // Always include a DSCSCheckResumability stage, both to verify that there is enough history to + // cover the change stream's starting point, and to swallow all events up to the resume point. + stages.push_back(DocumentSourceChangeStreamCheckResumability::create(expCtx, spec)); // If the pipeline is built on MongoS, we check for topology change events here. If a topology // change event is detected, this stage forwards the event directly to the executor via an @@ -324,7 +307,6 @@ std::list<boost::intrusive_ptr<DocumentSource>> DocumentSourceChangeStream::_bui stages.push_back(DocumentSourceChangeStreamCheckTopologyChange::create(expCtx)); } - // If 'fullDocumentBeforeChange' is not set to 'off', add the DSCSAddPreImage stage into the // pipeline. We place this stage here so that any $match stages which follow the $changeStream // pipeline may be able to skip ahead of the DSCSAddPreImage stage. This allows a whole-db or @@ -444,7 +426,7 @@ void DocumentSourceChangeStream::assertIsLegalSpecification( !spec.getResumeAfter() || !spec.getStartAfter()); auto resumeToken = (spec.getResumeAfter() || spec.getStartAfter()) - ? resolveResumeTokenFromSpec(expCtx, spec) + ? change_stream::resolveResumeTokenFromSpec(expCtx, spec) : boost::optional<ResumeTokenData>(); uassert(40674, diff --git a/src/mongo/db/pipeline/document_source_change_stream.h b/src/mongo/db/pipeline/document_source_change_stream.h index 60013e64444..044623335ed 100644 --- a/src/mongo/db/pipeline/document_source_change_stream.h +++ b/src/mongo/db/pipeline/document_source_change_stream.h @@ -243,9 +243,13 @@ public: // Default regex for collections match which prohibits system collections. static constexpr StringData kRegexAllCollections = R"((?!(\$|system\.)))"_sd; - // Regex matching all regular collections plus certain system collections. + + // Regex matching all user collections plus collections exposed when 'showSystemEvents' is set. + // Does not match a collection named $ or a collection with 'system.' in the name. + // However, it will still match collection names starting with system.buckets or a collection + // exactly named system.js. static constexpr StringData kRegexAllCollectionsShowSystemEvents = - R"((?!(\$|system\.(?!(js$)))))"_sd; + R"((?!(\$|system\.(?!(js$|buckets\.)))))"_sd; static constexpr StringData kRegexAllDBs = R"(^(?!(admin|config|local)\.)[^.]+)"_sd; static constexpr StringData kRegexCmdColl = R"(\$cmd$)"_sd; @@ -283,15 +287,6 @@ public: static void checkValueType(Value v, StringData fieldName, BSONType expectedType); /** - * Extracts the resume token from the given spec. If a 'startAtOperationTime' is specified, - * returns the equivalent high-watermark token. This method should only ever be called on a spec - * where one of 'resumeAfter', 'startAfter', or 'startAtOperationTime' is populated. - */ - static ResumeTokenData resolveResumeTokenFromSpec( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const DocumentSourceChangeStreamSpec& spec); - - /** * For a change stream with no resume information supplied by the user, returns the clusterTime * at which the new stream should begin scanning the oplog. */ diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp index 051922556a5..aa663dd131f 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_add_post_image.cpp @@ -48,6 +48,8 @@ REGISTER_INTERNAL_DOCUMENT_SOURCE(_internalChangeStreamAddPostImage, DocumentSourceChangeStreamAddPostImage::createFromBson, true); +constexpr auto makePostImageNotFoundErrorMsg = + &DocumentSourceChangeStreamAddPreImage::makePreImageNotFoundErrorMsg; Value assertFieldHasType(const Document& fullDoc, StringData fieldName, BSONType expectedType) { auto val = fullDoc[fieldName]; @@ -97,12 +99,11 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamAddPostImage::doGetNext( const auto postImageDoc = (_fullDocumentMode == FullDocumentModeEnum::kUpdateLookup ? lookupLatestPostImage(output.peek()) : generatePostImage(output.peek())); - uassert( - ErrorCodes::NoMatchingDocument, - str::stream() << "Change stream was configured to require a post-image for all update, " - "delete and replace events, but the post-image was not found for event: " - << output.peek().toString(), - postImageDoc || _fullDocumentMode != FullDocumentModeEnum::kRequired); + uassert(ErrorCodes::NoMatchingDocument, + str::stream() << "Change stream was configured to require a post-image for all update " + "events, but the post-image was not found for event: " + << makePostImageNotFoundErrorMsg(output.peek()), + postImageDoc || _fullDocumentMode != FullDocumentModeEnum::kRequired); // Even if no post-image was found, we have to populate the 'fullDocument' field. output[kFullDocumentFieldName] = (postImageDoc ? Value(*postImageDoc) : Value(BSONNULL)); diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp index 97b252c9829..42b0cc20bfb 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.cpp @@ -97,7 +97,7 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamAddPreImage::doGetNext() str::stream() << "Change stream was configured to require a pre-image for all update, delete " "and replace events, but pre-image id was not available for event: " - << input.getDocument().toString(), + << makePreImageNotFoundErrorMsg(input.getDocument()), _fullDocumentBeforeChangeMode != FullDocumentBeforeChangeModeEnum::kRequired); return input; } @@ -111,7 +111,7 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamAddPreImage::doGetNext() ErrorCodes::NoMatchingDocument, str::stream() << "Change stream was configured to require a pre-image for all update, " "delete and replace events, but the pre-image was not found for event: " - << input.getDocument().toString(), + << makePreImageNotFoundErrorMsg(input.getDocument()), preImageDoc || _fullDocumentBeforeChangeMode != FullDocumentBeforeChangeModeEnum::kRequired); @@ -165,4 +165,13 @@ Value DocumentSourceChangeStreamAddPreImage::serialize( DocumentSourceChangeStreamAddPreImageSpec(_fullDocumentBeforeChangeMode).toBSON()}}); } +std::string DocumentSourceChangeStreamAddPreImage::makePreImageNotFoundErrorMsg( + const Document& event) { + auto errMsgDoc = Document{{"operationType", event["operationType"]}, + {"ns", event["ns"]}, + {"clusterTime", event["clusterTime"]}, + {"txnNumber", event["txnNumber"]}}; + return errMsgDoc.toString(); +} + } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h index cc735fc135e..e04ac9a30ef 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h +++ b/src/mongo/db/pipeline/document_source_change_stream_add_pre_image.h @@ -63,6 +63,9 @@ public: static boost::optional<Document> lookupPreImage(boost::intrusive_ptr<ExpressionContext> pExpCtx, const Document& preImageId); + // Removes the internal fields from the event and returns the string representation of it. + static std::string makePreImageNotFoundErrorMsg(const Document& event); + DocumentSourceChangeStreamAddPreImage(const boost::intrusive_ptr<ExpressionContext>& expCtx, FullDocumentBeforeChangeModeEnum mode) : DocumentSource(kStageName, expCtx), _fullDocumentBeforeChangeMode(mode) { diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp b/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp index 8121fedb3e7..4e85815b049 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_check_invalidate.cpp @@ -31,6 +31,7 @@ #include "mongo/platform/basic.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/change_stream_start_after_invalidate_info.h" #include "mongo/db/pipeline/document_source_change_stream.h" #include "mongo/db/pipeline/document_source_change_stream_check_invalidate.h" @@ -71,7 +72,7 @@ DocumentSourceChangeStreamCheckInvalidate::create( const DocumentSourceChangeStreamSpec& spec) { // If resuming from an "invalidate" using "startAfter", pass along the resume token data to // DSCSCheckInvalidate to signify that another invalidate should not be generated. - auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); return new DocumentSourceChangeStreamCheckInvalidate( expCtx, boost::make_optional(resumeToken.fromInvalidate, std::move(resumeToken))); } diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp index 3861b21693d..e7e05dcd756 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.cpp @@ -30,6 +30,7 @@ #include "mongo/platform/basic.h" #include "mongo/db/curop.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/document_source_change_stream_check_resumability.h" #include "mongo/db/query/query_feature_flags_gen.h" #include "mongo/db/repl/oplog_entry.h" @@ -47,16 +48,15 @@ REGISTER_INTERNAL_DOCUMENT_SOURCE(_internalChangeStreamCheckResumability, // Returns ResumeStatus::kFoundToken if the document retrieved from the resumed pipeline satisfies // the client's resume token, ResumeStatus::kCheckNextDoc if it is older than the client's token, -// and ResumeToken::kSurpassedToken if it is more recent than the client's resume token (indicating -// that we will never see the token). +// and ResumeToken::kSurpassedToken if it is more recent than the client's resume token, indicating +// that we will never see the token. Return ResumeStatus::kNeedsSplit if we have found the event +// that produced the resume token, but it was split in the original stream. DocumentSourceChangeStreamCheckResumability::ResumeStatus DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( - const intrusive_ptr<ExpressionContext>& expCtx, - const Document& documentFromResumedStream, - const ResumeTokenData& tokenDataFromClient) { + const Document& eventFromResumedStream, const ResumeTokenData& tokenDataFromClient) { // Parse the stream doc into comprehensible ResumeTokenData. auto tokenDataFromResumedStream = - ResumeToken::parse(documentFromResumedStream["_id"].getDocument()).getData(); + ResumeToken::parse(eventFromResumedStream.metadata().getSortKey().getDocument()).getData(); // We start the resume with a $gte query on the timestamp, so we never expect it to be lower // than our resume token's timestamp. @@ -97,21 +97,25 @@ DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( // clusterTime. If the stream UUID sorts after the client's, however, then the stream is not // resumable; we are past the point in the stream where the token should have appeared. if (tokenDataFromResumedStream.uuid != tokenDataFromClient.uuid) { - // If we are running on a replica set deployment, we don't ever expect to see identical time - // stamps and txnOpIndex but differing UUIDs, and we reject the resume attempt at once. - if (!expCtx->inMongos && !expCtx->needsMerge) { - return ResumeStatus::kSurpassedToken; - } - // Otherwise, return a ResumeStatus based on the sort-order of the client and stream UUIDs. return tokenDataFromResumedStream.uuid > tokenDataFromClient.uuid ? ResumeStatus::kSurpassedToken : ResumeStatus::kCheckNextDoc; } - // If all the fields match exactly, then we have found the token. + // If the eventIdentifier matches exactly, then we have found the resume point. However, this + // event may have been split by the original stream; we must check the value of the resume + // token's fragmentNum field to determine the correct return status. if (ValueComparator::kInstance.evaluate(tokenDataFromResumedStream.eventIdentifier == tokenDataFromClient.eventIdentifier)) { - return ResumeStatus::kFoundToken; + if (tokenDataFromClient.fragmentNum && !tokenDataFromResumedStream.fragmentNum) { + return ResumeStatus::kNeedsSplit; + } + if (tokenDataFromResumedStream.fragmentNum == tokenDataFromClient.fragmentNum) { + return ResumeStatus::kFoundToken; + } + return tokenDataFromResumedStream.fragmentNum > tokenDataFromClient.fragmentNum + ? ResumeStatus::kSurpassedToken + : ResumeStatus::kCheckNextDoc; } // At this point, we know that the tokens differ only by eventIdentifier. The status we return @@ -130,7 +134,7 @@ DocumentSourceChangeStreamCheckResumability::DocumentSourceChangeStreamCheckResu intrusive_ptr<DocumentSourceChangeStreamCheckResumability> DocumentSourceChangeStreamCheckResumability::create(const intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec) { - auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); return new DocumentSourceChangeStreamCheckResumability(expCtx, std::move(resumeToken)); } @@ -178,15 +182,21 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamCheckResumability::doGet // Determine whether the current event sorts before, equal to or after the resume token. _resumeStatus = DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( - pExpCtx, nextInput.getDocument(), _tokenFromClient); + nextInput.getDocument(), _tokenFromClient); switch (_resumeStatus) { case ResumeStatus::kCheckNextDoc: // If the result was kCheckNextDoc, we are resumable but must swallow this event. continue; + case ResumeStatus::kNeedsSplit: + // If the result was kNeedsSplit, we found a resume token which matches the client's + // except for the splitNum attribute. Allow this document to pass through so that + // the split stage can regenerate the original fragments and their resume tokens. + return nextInput; case ResumeStatus::kSurpassedToken: // In this case the resume token wasn't found; it may be on another shard. However, // since the oplog scan did not throw, we know that we are resumable. Fall through // into the following case and return the document. + return nextInput; case ResumeStatus::kFoundToken: // We found the actual token! Return the doc so DSEnsureResumeTokenPresent sees it. return nextInput; diff --git a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h index a290d59cacd..59946310ab1 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h +++ b/src/mongo/db/pipeline/document_source_change_stream_check_resumability.h @@ -68,7 +68,8 @@ public: enum class ResumeStatus { kFoundToken, // The stream produced a document satisfying the client resume token. kSurpassedToken, // The stream's latest document is more recent than the resume token. - kCheckNextDoc // The next document produced by the stream may contain the resume token. + kCheckNextDoc, // The next document produced by the stream may contain the resume token. + kNeedsSplit // We found a candidate resume token but the event must be split. }; const char* getSourceName() const override; @@ -98,10 +99,8 @@ public: const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec); - static ResumeStatus compareAgainstClientResumeToken( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const Document& documentFromResumedStream, - const ResumeTokenData& tokenDataFromClient); + static ResumeStatus compareAgainstClientResumeToken(const Document& eventFromResumedStream, + const ResumeTokenData& tokenDataFromClient); protected: /** diff --git a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp index 15fcb30d58f..420f7313161 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.cpp @@ -31,6 +31,7 @@ #include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/change_stream_start_after_invalidate_info.h" #include "mongo/db/query/query_feature_flags_gen.h" @@ -45,7 +46,7 @@ boost::intrusive_ptr<DocumentSourceChangeStreamEnsureResumeTokenPresent> DocumentSourceChangeStreamEnsureResumeTokenPresent::create( const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec) { - auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); tassert(5666902, "Expected non-high-water-mark resume token", !ResumeToken::isHighWaterMarkToken(resumeToken)); @@ -137,9 +138,10 @@ DocumentSource::GetNextResult DocumentSourceChangeStreamEnsureResumeTokenPresent const DocumentSource::GetNextResult nextInput = Document::fromBsonWithMetaData(extraInfo->getStartAfterInvalidateEvent()); + _resumeStatus = DocumentSourceChangeStreamCheckResumability::compareAgainstClientResumeToken( - pExpCtx, nextInput.getDocument(), _tokenFromClient); + nextInput.getDocument(), _tokenFromClient); // This exception should always contain the client-provided resume token. tassert(5779201, diff --git a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h index 3f24ed446ea..6fcabf4c0b6 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h +++ b/src/mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h @@ -47,7 +47,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // This stage neither modifies nor renames any field. - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } static boost::intrusive_ptr<DocumentSourceChangeStreamEnsureResumeTokenPresent> create( diff --git a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp index 0f32b20aa47..b75009c2577 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.cpp @@ -228,7 +228,8 @@ BSONObj DocumentSourceChangeStreamHandleTopologyChange::createUpdatedCommandForN Document{shardCommand}, splitPipelines, boost::none, /* exhangeSpec */ - true /* needsMerge */); + true /* needsMerge */, + boost::none /* explain */); } BSONObj DocumentSourceChangeStreamHandleTopologyChange::replaceResumeTokenInCommand( diff --git a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h index c5d5a16fd93..4c07368aff9 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h +++ b/src/mongo/db/pipeline/document_source_change_stream_handle_topology_change.h @@ -67,7 +67,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // This stage neither modifies nor renames any field. - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() final { diff --git a/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp b/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp index c11cb0a1aeb..658861a9a3f 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_oplog_match.cpp @@ -31,6 +31,7 @@ #include "mongo/bson/bsonmisc.h" #include "mongo/db/pipeline/change_stream_filter_helpers.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/document_source_change_stream_unwind_transaction.h" namespace mongo { @@ -104,7 +105,7 @@ DocumentSourceChangeStreamOplogMatch::DocumentSourceChangeStreamOplogMatch( boost::intrusive_ptr<DocumentSourceChangeStreamOplogMatch> DocumentSourceChangeStreamOplogMatch::create(const boost::intrusive_ptr<ExpressionContext>& expCtx, const DocumentSourceChangeStreamSpec& spec) { - auto resumeToken = DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); return make_intrusive<DocumentSourceChangeStreamOplogMatch>(resumeToken.clusterTime, expCtx); } diff --git a/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp new file mode 100644 index 00000000000..b2ac5353325 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.cpp @@ -0,0 +1,236 @@ +/** + * Copyright (C) 2023-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/document_source_change_stream_split_large_event.h" + +#include "mongo/db/pipeline/change_stream_helpers.h" +#include "mongo/db/pipeline/change_stream_split_event_helpers.h" +#include "mongo/db/pipeline/document_source_change_stream_ensure_resume_token_present.h" +#include "mongo/db/pipeline/document_source_change_stream_handle_topology_change.h" + +namespace mongo { +namespace { +Counter64 changeStreamsLargeEventsSplitCounter; +ServerStatusMetricField<Counter64> dchangeStreamsLargeEventsSplitCounter( + "changeStreams.largeEventsSplit", &changeStreamsLargeEventsSplitCounter); +} // namespace +REGISTER_DOCUMENT_SOURCE(changeStreamSplitLargeEvent, + DocumentSourceChangeStreamSplitLargeEvent::LiteParsed::parse, + DocumentSourceChangeStreamSplitLargeEvent::createFromBson, + AllowedWithApiStrict::kNeverInVersion1); + +boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> +DocumentSourceChangeStreamSplitLargeEvent::create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const DocumentSourceChangeStreamSpec& spec) { + // If resuming from a split event, pass along the resume token data to DSCSSplitEvent so that it + // can swallow fragments that precede the actual resume point. + auto resumeToken = change_stream::resolveResumeTokenFromSpec(expCtx, spec); + auto resumeAfterSplit = + resumeToken.fragmentNum ? std::move(resumeToken) : boost::optional<ResumeTokenData>{}; + return new DocumentSourceChangeStreamSplitLargeEvent(expCtx, std::move(resumeAfterSplit)); +} + +boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> +DocumentSourceChangeStreamSplitLargeEvent::createFromBson( + BSONElement rawSpec, const boost::intrusive_ptr<ExpressionContext>& expCtx) { + // We expect an empty object spec for this stage. + uassert(7182800, + "$changeStreamSplitLargeEvent spec should be an empty object", + rawSpec.type() == BSONType::Object && rawSpec.Obj().isEmpty()); + + // If there is no change stream spec set on the expression context, then this cannot be a change + // stream pipeline. Pipeline validation will catch this issue later during parsing. + if (!expCtx->changeStreamSpec) { + return new DocumentSourceChangeStreamSplitLargeEvent(expCtx, boost::none); + } + return create(expCtx, *expCtx->changeStreamSpec); +} + +DocumentSourceChangeStreamSplitLargeEvent::DocumentSourceChangeStreamSplitLargeEvent( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<ResumeTokenData> resumeAfterSplit) + : DocumentSource(getSourceName(), expCtx), _resumeAfterSplit(std::move(resumeAfterSplit)) { + tassert(7182801, + "Expected a split event resume token, but found a non-split token", + !_resumeAfterSplit || _resumeAfterSplit->fragmentNum); +} + +Value DocumentSourceChangeStreamSplitLargeEvent::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(Document{{DocumentSourceChangeStreamSplitLargeEvent::kStageName, Document{}}}); +} + +StageConstraints DocumentSourceChangeStreamSplitLargeEvent::constraints( + Pipeline::SplitState pipeState) const { + StageConstraints constraints{StreamType::kStreaming, + PositionRequirement::kCustom, + HostTypeRequirement::kAnyShard, + DiskUseRequirement::kNoDiskUse, + FacetRequirement::kNotAllowed, + TransactionRequirement::kNotAllowed, + LookupRequirement::kNotAllowed, + UnionRequirement::kNotAllowed, + ChangeStreamRequirement::kRequiresChangeStream}; + + // The user cannot specify multiple split stages in the pipeline. + constraints.canAppearOnlyOnceInPipeline = true; + return constraints; +} + +DocumentSource::GetModPathsReturn DocumentSourceChangeStreamSplitLargeEvent::getModifiedPaths() + const { + // This stage may modify the entire document. + return {GetModPathsReturn::Type::kAllPaths, {}, {}}; +} + +DocumentSource::GetNextResult DocumentSourceChangeStreamSplitLargeEvent::doGetNext() { + // If we've already queued up some fragments, return them. + if (!_splitEventQueue.empty()) { + return _popFromQueue(); + } + + auto input = pSource->getNext(); + + // If the next result is EOF return, it as-is. + if (!input.isAdvanced()) { + return input; + } + + // Process the event to see if it is within the size limit. We have to serialize the document to + // perform this check, but the helper will also produce a new 'Document' which - if it is small + // enough to be returned - will not need to be re-serialized by the plan executor. + auto [eventDoc, eventBsonSize] = change_stream_split_event::processChangeEventBeforeSplit( + input.getDocument(), this->pExpCtx->needsMerge || this->pExpCtx->forPerShardCursor); + + // Make sure to leave some space for the postBatchResumeToken in the cursor response object. + size_t tokenSize = eventDoc.metadata().getSortKey().getDocument().toBson().objsize(); + + // If we are resuming from a split event, check whether this is it. If so, extract the fragment + // number from which we are resuming. Otherwise, we have already scanned past the resume point, + // which implies that it may be on another shard. Continue to split this event without skipping. + size_t skipFragments = _handleResumeAfterSplit(eventDoc, eventBsonSize + tokenSize); + + // Before proceeding, check whether the event is small enough to be returned as-is. + if (eventBsonSize + tokenSize <= kBSONObjMaxChangeEventSize) { + return std::move(eventDoc); + } + + // Split the event into N appropriately-sized fragments. + _splitEventQueue = change_stream_split_event::splitChangeEvent( + eventDoc, kBSONObjMaxChangeEventSize - tokenSize, skipFragments); + + // If the user is resuming from a split event but supplied a pipeline which produced a different + // split, we cannot reproduce the split point. Check if we're about to swallow all fragments. + uassert(ErrorCodes::ChangeStreamFatalError, + "Attempted to resume from a split event, but the resumed stream produced a different " + "split. Ensure that the pipeline used to resume is the same as the original", + !(skipFragments > 0 && _splitEventQueue.empty())); + tassert(7182804, + "Unexpected empty fragment queue after splitting a change stream event", + !_splitEventQueue.empty()); + + // Increment the ServerStatus counter to indicate that we have split a change event. + changeStreamsLargeEventsSplitCounter.increment(); + + // Return the first element from the queue of fragments. + return _popFromQueue(); +} + +Document DocumentSourceChangeStreamSplitLargeEvent::_popFromQueue() { + auto nextFragment = std::move(_splitEventQueue.front()); + _splitEventQueue.pop(); + return nextFragment; +} + +size_t DocumentSourceChangeStreamSplitLargeEvent::_handleResumeAfterSplit(const Document& eventDoc, + size_t eventBsonSize) { + if (!_resumeAfterSplit) { + return 0; + } + using DSCSCR = DocumentSourceChangeStreamCheckResumability; + auto resumeStatus = DSCSCR::compareAgainstClientResumeToken(eventDoc, *_resumeAfterSplit); + tassert(7182805, + "Observed unexpected event before resume point", + resumeStatus != DSCSCR::ResumeStatus::kCheckNextDoc); + uassert(ErrorCodes::ChangeStreamFatalError, + "Attempted to resume from a split event fragment, but the event in the resumed " + "stream was not large enough to be split", + resumeStatus != DSCSCR::ResumeStatus::kNeedsSplit || + eventBsonSize > kBSONObjMaxChangeEventSize); + auto fragmentNum = + (resumeStatus == DSCSCR::ResumeStatus::kNeedsSplit ? *_resumeAfterSplit->fragmentNum : 0); + _resumeAfterSplit.reset(); + return fragmentNum; +} + +namespace { +// During pipeline optimization, the split stage must move ahead of these change stream stages. +static const std::set<StringData> kStagesToMoveAheadOf = { + DocumentSourceChangeStreamEnsureResumeTokenPresent::kStageName, + DocumentSourceChangeStreamHandleTopologyChange::kStageName}; +} // namespace + +Pipeline::SourceContainer::iterator DocumentSourceChangeStreamSplitLargeEvent::doOptimizeAt( + Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { + // Helper to determine whether the iterator has reached its final position in the pipeline. + // Checks whether $changeStreamSplitLargeEvent should move ahead of the given stage. + auto shouldMoveAheadOf = [](const auto& stagePtr) { + return kStagesToMoveAheadOf.count(stagePtr->getSourceName()); + }; + + // Find the point in the pipeline that the stage should move to. + for (auto it = itr; it != container->begin() && shouldMoveAheadOf(*std::prev(it)); --it) { + std::swap(*it, *std::prev(it)); + } + + // Return an iterator pointing to the next stage to be optimized. + return std::next(itr); +} + +void DocumentSourceChangeStreamSplitLargeEvent::validatePipelinePosition( + bool alreadyOptimized, + Pipeline::SourceContainer::const_iterator pos, + const Pipeline::SourceContainer& container) const { + // The $changeStreamSplitLargeEvent stage must be the final stage in the pipeline before + // optimization. + uassert(7182802, + str::stream() << getSourceName() << " must be the last stage in the pipeline", + alreadyOptimized || pos == std::prev(container.cend())); + + // The $changeStreamSplitLargeEvent stage must not be after 'kStagesToMoveAheadOf' stages after + // optimization. + uassert(7182803, + str::stream() << getSourceName() + << " is at the wrong position in the pipeline after optimization", + !alreadyOptimized || std::none_of(container.begin(), pos, [](const auto& stage) { + return kStagesToMoveAheadOf.count(stage->getSourceName()); + })); +}; +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h new file mode 100644 index 00000000000..1dfab7b85a1 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_change_stream_split_large_event.h @@ -0,0 +1,123 @@ +/** + * Copyright (C) 2023-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <queue> + +#include "mongo/db/pipeline/document_source.h" + +namespace mongo { + +class DocumentSourceChangeStreamSplitLargeEvent : public DocumentSource { +public: + class LiteParsed : public LiteParsedDocumentSource { + public: + static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, + const BSONElement& spec) { + uassert(7182899, + str::stream() + << "$changeStreamSplitLargeEvent must take a nested object but found: " + << spec, + spec.type() == BSONType::Object); + return std::make_unique<LiteParsed>(spec.fieldName()); + } + + explicit LiteParsed(std::string parseTimeName) + : LiteParsedDocumentSource(std::move(parseTimeName)) {} + + bool isChangeStreamSplitLargeEvent() const final { + return true; + } + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return {}; + } + + PrivilegeVector requiredPrivileges(bool isMongos, + bool bypassDocumentValidation) const final { + return {}; + } + }; + + static constexpr StringData kStageName = "$changeStreamSplitLargeEvent"_sd; + static constexpr size_t kBSONObjMaxChangeEventSize = BSONObjMaxInternalSize - (8 * 1024); + + static boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const DocumentSourceChangeStreamSpec& spec); + + static boost::intrusive_ptr<DocumentSourceChangeStreamSplitLargeEvent> createFromBson( + BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); + + DocumentSource::GetModPathsReturn getModifiedPaths() const final; + + // This stage does not reference any user or system variables. + void addVariableRefs(std::set<Variables::Id>* refs) const {} + + Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final; + + StageConstraints constraints(Pipeline::SplitState pipeState) const final; + + void validatePipelinePosition(bool alreadyOptimized, + Pipeline::SourceContainer::const_iterator pos, + const Pipeline::SourceContainer& container) const final; + + boost::optional<DistributedPlanLogic> distributedPlanLogic() final { + return boost::none; + } + + const char* getSourceName() const final { + return kStageName.rawData(); + } + +protected: + Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container) final; + + DocumentSource::GetNextResult doGetNext() final; + +private: + // This constructor is private, callers should use the 'create()' method above. + DocumentSourceChangeStreamSplitLargeEvent(const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<ResumeTokenData> resumeAfterSplit); + + Document _popFromQueue(); + + /** + * In case of resume after split, check whether 'eventDoc' is the split event. If so, extract + * and return the resume token's fragment number. Otherwise, return zero. + */ + size_t _handleResumeAfterSplit(const Document& eventDoc, size_t eventBsonSize); + + boost::optional<ResumeTokenData> _resumeAfterSplit; + std::queue<Document> _splitEventQueue; +}; + +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_change_stream_transform.cpp b/src/mongo/db/pipeline/document_source_change_stream_transform.cpp index 766150a6e54..7434190a609 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_transform.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_transform.cpp @@ -33,6 +33,7 @@ #include "mongo/db/pipeline/document_source_change_stream_transform.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/pipeline/resume_token.h" @@ -63,6 +64,10 @@ DocumentSourceChangeStreamTransform::createFromBson( rawSpec.type() == BSONType::Object); auto spec = DocumentSourceChangeStreamSpec::parse(IDLParserErrorContext("$changeStream"), rawSpec.Obj()); + + // Set the change stream spec on the expression context. + expCtx->changeStreamSpec = spec; + return new DocumentSourceChangeStreamTransform(expCtx, std::move(spec)); } @@ -74,8 +79,7 @@ DocumentSourceChangeStreamTransform::DocumentSourceChangeStreamTransform( _isIndependentOfAnyCollection(expCtx->ns.isCollectionlessAggregateNS()) { // Extract the resume token or high-water-mark from the spec. - auto tokenData = - DocumentSourceChangeStream::resolveResumeTokenFromSpec(expCtx, _changeStreamSpec); + auto tokenData = change_stream::resolveResumeTokenFromSpec(expCtx, _changeStreamSpec); // Set the initialPostBatchResumeToken on the expression context. expCtx->initialPostBatchResumeToken = ResumeToken(tokenData).toBSON(); @@ -118,7 +122,7 @@ DepsTracker::State DocumentSourceChangeStreamTransform::getDependencies(DepsTrac DocumentSource::GetModPathsReturn DocumentSourceChangeStreamTransform::getModifiedPaths() const { // All paths are modified. - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; } DocumentSource::GetNextResult DocumentSourceChangeStreamTransform::doGetNext() { diff --git a/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp b/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp index 05bfcff4df7..ae65c696d65 100644 --- a/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp +++ b/src/mongo/db/pipeline/document_source_change_stream_unwind_transaction.cpp @@ -157,7 +157,7 @@ DepsTracker::State DocumentSourceChangeStreamUnwindTransaction::getDependencies( DocumentSource::GetModPathsReturn DocumentSourceChangeStreamUnwindTransaction::getModifiedPaths() const { - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; } DocumentSource::GetNextResult DocumentSourceChangeStreamUnwindTransaction::doGetNext() { diff --git a/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp b/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp index 28603f10a30..2d5eaf32521 100644 --- a/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp +++ b/src/mongo/db/pipeline/document_source_check_resume_token_test.cpp @@ -206,10 +206,6 @@ protected: if (!_collScan) { _collScan = std::make_unique<CollectionScan>( pExpCtx.get(), _collectionPtr, _params, &_ws, _filter.get()); - // The first call to doWork will create the cursor and return NEED_TIME. But it won't - // actually scan any of the documents that are present in the mock cursor queue. - ASSERT_EQ(_collScan->doWork(nullptr), PlanStage::NEED_TIME); - ASSERT_EQ(_getNumDocsTested(), 0); } while (true) { // If the next result is a pause, return it and don't collscan. @@ -228,6 +224,7 @@ protected: // entry into the oplog. This is like a stripped-down DSCSTransform stage. MutableDocument mutableDoc{_ws.get(id)->doc.value()}; mutableDoc["_id"] = nextResult.getDocument()["_id"]; + mutableDoc.metadata().setSortKey(nextResult.getDocument()["_id"], true); return mutableDoc.freeze(); } case PlanStage::NEED_TIME: @@ -483,8 +480,12 @@ TEST_F(CheckResumeTokenTest, ShouldFailIfTokenHasWrongNamespace) { Timestamp resumeTimestamp(100, 1); auto resumeTokenUUID = UUID::gen(); - auto checkResumeToken = createDSEnsureResumeTokenPresent(resumeTimestamp, "1", resumeTokenUUID); auto otherUUID = UUID::gen(); + ASSERT_NE(resumeTokenUUID, otherUUID); + if (resumeTokenUUID > otherUUID) { + std::swap(resumeTokenUUID, otherUUID); + } + auto checkResumeToken = createDSEnsureResumeTokenPresent(resumeTimestamp, "1", resumeTokenUUID); addOplogEntryOnTestNS(resumeTimestamp, "1", otherUUID); ASSERT_THROWS_CODE( checkResumeToken->getNext(), AssertionException, ErrorCodes::ChangeStreamFatalError); diff --git a/src/mongo/db/pipeline/document_source_coll_stats.cpp b/src/mongo/db/pipeline/document_source_coll_stats.cpp index c00f5fcd44d..2881425d5e4 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.cpp +++ b/src/mongo/db/pipeline/document_source_coll_stats.cpp @@ -71,18 +71,15 @@ intrusive_ptr<DocumentSource> DocumentSourceCollStats::createFromBson( return make_intrusive<DocumentSourceCollStats>(pExpCtx, std::move(spec)); } -DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() { - if (_finished) { - return GetNextResult::makeEOF(); - } - - _finished = true; - +BSONObj DocumentSourceCollStats::makeStatsForNs( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& nss, + const DocumentSourceCollStatsSpec& spec) { BSONObjBuilder builder; - builder.append("ns", pExpCtx->ns.ns()); + builder.append("ns", nss.ns()); - auto shardName = pExpCtx->mongoProcessInterface->getShardName(pExpCtx->opCtx); + auto shardName = expCtx->mongoProcessInterface->getShardName(expCtx->opCtx); if (!shardName.empty()) { builder.append("shard", shardName); @@ -91,33 +88,45 @@ DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() { builder.append("host", getHostNameCachedAndPort()); builder.appendDate("localTime", jsTime()); - if (auto latencyStatsSpec = _collStatsSpec.getLatencyStats()) { - pExpCtx->mongoProcessInterface->appendLatencyStats( - pExpCtx->opCtx, pExpCtx->ns, latencyStatsSpec->getHistograms(), &builder); + if (auto latencyStatsSpec = spec.getLatencyStats()) { + // getRequestOnTimeseriesView is set to true if collstats is called on the view. + auto resolvedNss = + spec.getRequestOnTimeseriesView() ? nss.getTimeseriesViewNamespace() : nss; + expCtx->mongoProcessInterface->appendLatencyStats( + expCtx->opCtx, resolvedNss, latencyStatsSpec->getHistograms(), &builder); } - if (auto storageStats = _collStatsSpec.getStorageStats()) { + if (auto storageStats = spec.getStorageStats()) { // If the storageStats field exists, it must have been validated as an object when parsing. BSONObjBuilder storageBuilder(builder.subobjStart("storageStats")); - uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendStorageStats( - pExpCtx->opCtx, pExpCtx->ns, *storageStats, &storageBuilder), + uassertStatusOKWithContext(expCtx->mongoProcessInterface->appendStorageStats( + expCtx->opCtx, nss, *storageStats, &storageBuilder), "Unable to retrieve storageStats in $collStats stage"); storageBuilder.doneFast(); } - if (_collStatsSpec.getCount()) { - uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendRecordCount( - pExpCtx->opCtx, pExpCtx->ns, &builder), - "Unable to retrieve count in $collStats stage"); + if (spec.getCount()) { + uassertStatusOKWithContext( + expCtx->mongoProcessInterface->appendRecordCount(expCtx->opCtx, nss, &builder), + "Unable to retrieve count in $collStats stage"); } - if (_collStatsSpec.getQueryExecStats()) { - uassertStatusOKWithContext(pExpCtx->mongoProcessInterface->appendQueryExecStats( - pExpCtx->opCtx, pExpCtx->ns, &builder), - "Unable to retrieve queryExecStats in $collStats stage"); + if (spec.getQueryExecStats()) { + uassertStatusOKWithContext( + expCtx->mongoProcessInterface->appendQueryExecStats(expCtx->opCtx, nss, &builder), + "Unable to retrieve queryExecStats in $collStats stage"); } + return builder.obj(); +} + +DocumentSource::GetNextResult DocumentSourceCollStats::doGetNext() { + if (_finished) { + return GetNextResult::makeEOF(); + } + + _finished = true; - return {Document(builder.obj())}; + return {Document(makeStatsForNs(pExpCtx, pExpCtx->ns, _collStatsSpec))}; } Value DocumentSourceCollStats::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { diff --git a/src/mongo/db/pipeline/document_source_coll_stats.h b/src/mongo/db/pipeline/document_source_coll_stats.h index 72c6923ccdc..074683367e1 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.h +++ b/src/mongo/db/pipeline/document_source_coll_stats.h @@ -85,6 +85,10 @@ public: const DocumentSourceCollStatsSpec _spec; }; + static BSONObj makeStatsForNs(const boost::intrusive_ptr<ExpressionContext>&, + const NamespaceString&, + const DocumentSourceCollStatsSpec&); + DocumentSourceCollStats(const boost::intrusive_ptr<ExpressionContext>& pExpCtx, DocumentSourceCollStatsSpec spec) : DocumentSource(kStageName, pExpCtx), _collStatsSpec(std::move(spec)) {} diff --git a/src/mongo/db/pipeline/document_source_coll_stats.idl b/src/mongo/db/pipeline/document_source_coll_stats.idl index 060c20d835a..264a4004a5c 100644 --- a/src/mongo/db/pipeline/document_source_coll_stats.idl +++ b/src/mongo/db/pipeline/document_source_coll_stats.idl @@ -39,7 +39,7 @@ structs: LatencyStatsSpec: description: Represents the 'latencyStats' argument to the $collStats stage. strict: true - fields: + fields: histograms: description: Adds latency histogram information to the embedded documents in latencyStats if true. type: optionalBool @@ -67,3 +67,8 @@ structs: validator: callback: validateObjectIsEmpty optional: true + $_requestOnTimeseriesView: + description: When set to true, $collStats stage requests statistics from the view namespace. + When set to false, $collStats stage requests statistics from the underlying collection. + cpp_name: requestOnTimeseriesView + type: optionalBool diff --git a/src/mongo/db/pipeline/document_source_densify.cpp b/src/mongo/db/pipeline/document_source_densify.cpp index 740ef193431..ab43e159692 100644 --- a/src/mongo/db/pipeline/document_source_densify.cpp +++ b/src/mongo/db/pipeline/document_source_densify.cpp @@ -335,10 +335,19 @@ DocumentSource::GetNextResult DocumentSourceInternalDensify::densifyExplicitRang RangeStatement(_range.getStep(), ExplicitBounds(bounds.first, bounds.second), _range.getUnit())); + } else if (_current < bounds.first) { + // All the documents we saw were below the explicit range, so _current is below the range. + // Densification starts at the first bounds, so _current is no longer relevant. + createDocGenerator(bounds.first, + RangeStatement(_range.getStep(), + ExplicitBounds(bounds.first, bounds.second), + _range.getUnit())); + } else if (_current->increment(_range) >= bounds.second) { _densifyState = DensifyState::kDensifyDone; return DocumentSource::GetNextResult::makeEOF(); } else { + // _current is somewhere in the middle of the range. auto lowerBound = _current->increment(_range); createDocGenerator(lowerBound, RangeStatement(_range.getStep(), diff --git a/src/mongo/db/pipeline/document_source_documents.cpp b/src/mongo/db/pipeline/document_source_documents.cpp index 5d4e02c71ea..934116a7c27 100644 --- a/src/mongo/db/pipeline/document_source_documents.cpp +++ b/src/mongo/db/pipeline/document_source_documents.cpp @@ -44,7 +44,7 @@ namespace mongo { using boost::intrusive_ptr; REGISTER_DOCUMENT_SOURCE(documents, - LiteParsedDocumentSourceDefault::parse, + DocumentSourceDocuments::LiteParsed::parse, DocumentSourceDocuments::createFromBson, AllowedWithApiStrict::kAlways); diff --git a/src/mongo/db/pipeline/document_source_documents.h b/src/mongo/db/pipeline/document_source_documents.h index a6ab70e0c59..6048ed551bb 100644 --- a/src/mongo/db/pipeline/document_source_documents.h +++ b/src/mongo/db/pipeline/document_source_documents.h @@ -35,6 +35,30 @@ namespace mongo { namespace DocumentSourceDocuments { +class LiteParsed : public LiteParsedDocumentSource { +public: + static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, const BSONElement& spec) { + return std::make_unique<LiteParsed>(spec.fieldName()); + } + + LiteParsed(std::string parseTimeName) : LiteParsedDocumentSource(std::move(parseTimeName)) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return stdx::unordered_set<NamespaceString>(); + } + + PrivilegeVector requiredPrivileges(bool isMongos, bool bypassDocumentValidation) const final { + return {}; + } + + bool isDocuments() const final { + return true; + } + + bool allowedToPassthroughFromMongos() const final { + return false; + } +}; static constexpr StringData kStageName = "$documents"_sd; diff --git a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp index 2fc1208e2b9..13229ba6d3e 100644 --- a/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_find_and_modify_image_lookup.cpp @@ -199,7 +199,7 @@ DepsTracker::State DocumentSourceFindAndModifyImageLookup::getDependencies( } DocumentSource::GetModPathsReturn DocumentSourceFindAndModifyImageLookup::getModifiedPaths() const { - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; } DocumentSource::GetNextResult DocumentSourceFindAndModifyImageLookup::doGetNext() { diff --git a/src/mongo/db/pipeline/document_source_geo_near.h b/src/mongo/db/pipeline/document_source_geo_near.h index 83ddd02bb88..2b012285560 100644 --- a/src/mongo/db/pipeline/document_source_geo_near.h +++ b/src/mongo/db/pipeline/document_source_geo_near.h @@ -51,7 +51,7 @@ public: StageConstraints constraints(Pipeline::SplitState pipeState) const final { return {StreamType::kStreaming, - PositionRequirement::kFirstAfterOptimization, + PositionRequirement::kCustom, HostTypeRequirement::kAnyShard, DiskUseRequirement::kNoDiskUse, FacetRequirement::kNotAllowed, @@ -60,6 +60,17 @@ public: UnionRequirement::kAllowed}; } + void validatePipelinePosition(bool alreadyOptimized, + Pipeline::SourceContainer::const_iterator pos, + const Pipeline::SourceContainer& container) const final { + // This stage must be in the first position in the pipeline after optimization. + uassert(40603, + str::stream() << getSourceName() + << " was not the first stage in the pipeline after optimization. Is " + "optimization disabled or inhibited?", + !alreadyOptimized || pos == container.cbegin()); + } + /** * DocumentSourceGeoNear should always be replaced by a DocumentSourceGeoNearCursor before * executing a pipeline, so this method should never be called. diff --git a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp index 27e72886225..8de9f300ab9 100644 --- a/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp +++ b/src/mongo/db/pipeline/document_source_geo_near_cursor.cpp @@ -77,9 +77,6 @@ DocumentSourceGeoNearCursor::DocumentSourceGeoNearCursor( _distanceField(std::move(distanceField)), _locationField(std::move(locationField)), _distanceMultiplier(distanceMultiplier) { - tassert(6466203, - "$geoNear cursor shouldn't have secondary collections", - collections.getSecondaryCollections().empty()); invariant(_distanceMultiplier >= 0); } diff --git a/src/mongo/db/pipeline/document_source_graph_lookup.cpp b/src/mongo/db/pipeline/document_source_graph_lookup.cpp index 417e92df4b0..8c1076d3d76 100644 --- a/src/mongo/db/pipeline/document_source_graph_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_graph_lookup.cpp @@ -507,7 +507,7 @@ void DocumentSourceGraphLookUp::performSearch() { } DocumentSource::GetModPathsReturn DocumentSourceGraphLookUp::getModifiedPaths() const { - std::set<std::string> modifiedPaths{_as.fullPath()}; + OrderedPathSet modifiedPaths{_as.fullPath()}; if (_unwind) { auto pathsModifiedByUnwind = _unwind.get()->getModifiedPaths(); invariant(pathsModifiedByUnwind.type == GetModPathsReturn::Type::kFiniteSet); @@ -623,6 +623,7 @@ DocumentSourceGraphLookUp::DocumentSourceGraphLookUp( _variablesParseState(expCtx->variablesParseState.copyWith(_variables.useIdGenerator())) { const auto& resolvedNamespace = pExpCtx->getResolvedNamespace(_from); _fromExpCtx = pExpCtx->copyForSubPipeline(resolvedNamespace.ns, resolvedNamespace.uuid); + _fromExpCtx->inLookup = true; // We append an additional BSONObj to '_fromPipeline' as a placeholder for the $match stage // we'll eventually construct from the input document. diff --git a/src/mongo/db/pipeline/document_source_graph_lookup.h b/src/mongo/db/pipeline/document_source_graph_lookup.h index bc0146f1829..a4ad79c8111 100644 --- a/src/mongo/db/pipeline/document_source_graph_lookup.h +++ b/src/mongo/db/pipeline/document_source_graph_lookup.h @@ -132,6 +132,10 @@ public: DepsTracker::State getDependencies(DepsTracker* deps) const final { _startWith->addDependencies(deps); + if (_additionalFilter) { + uassertStatusOK(MatchExpressionParser::parse(*_additionalFilter, _fromExpCtx)) + ->addDependencies(deps); + } return DepsTracker::State::SEE_NEXT; }; diff --git a/src/mongo/db/pipeline/document_source_group.cpp b/src/mongo/db/pipeline/document_source_group.cpp index 3f718360838..f437debe890 100644 --- a/src/mongo/db/pipeline/document_source_group.cpp +++ b/src/mongo/db/pipeline/document_source_group.cpp @@ -45,82 +45,6 @@ namespace mongo { -namespace { - -/** - * Generates a new file name on each call using a static, atomic and monotonically increasing - * number. - * - * Each user of the Sorter must implement this function to ensure that all temporary files that the - * Sorter instances produce are uniquely identified using a unique file name extension with separate - * atomic variable. This is necessary because the sorter.cpp code is separately included in multiple - * places, rather than compiled in one place and linked, and so cannot provide a globally unique ID. - */ -std::string nextFileName() { - static AtomicWord<unsigned> documentSourceGroupFileCounter; - return "extsort-doc-group." + std::to_string(documentSourceGroupFileCounter.fetchAndAdd(1)); -} - -} // namespace - -using boost::intrusive_ptr; -using std::pair; -using std::shared_ptr; -using std::vector; - -Document GroupFromFirstDocumentTransformation::applyTransformation(const Document& input) { - MutableDocument output(_accumulatorExprs.size()); - - for (auto&& expr : _accumulatorExprs) { - auto value = expr.second->evaluate(input, &expr.second->getExpressionContext()->variables); - output.addField(expr.first, value.missing() ? Value(BSONNULL) : std::move(value)); - } - - return output.freeze(); -} - -void GroupFromFirstDocumentTransformation::optimize() { - for (auto&& expr : _accumulatorExprs) { - expr.second = expr.second->optimize(); - } -} - -Document GroupFromFirstDocumentTransformation::serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const { - - MutableDocument newRoot(_accumulatorExprs.size()); - for (auto&& expr : _accumulatorExprs) { - newRoot.addField(expr.first, expr.second->serialize(static_cast<bool>(explain))); - } - - return {{"newRoot", newRoot.freezeToValue()}}; -} - -DepsTracker::State GroupFromFirstDocumentTransformation::addDependencies(DepsTracker* deps) const { - for (auto&& expr : _accumulatorExprs) { - expr.second->addDependencies(deps); - } - - // This stage will replace the entire document with a new document, so any existing fields - // will be replaced and cannot be required as dependencies. We use EXHAUSTIVE_ALL here - // instead of EXHAUSTIVE_FIELDS, as in ReplaceRootTransformation, because the stages that - // follow a $group stage should not depend on document metadata. - return DepsTracker::State::EXHAUSTIVE_ALL; -} - -DocumentSource::GetModPathsReturn GroupFromFirstDocumentTransformation::getModifiedPaths() const { - // Replaces the entire root, so all paths are modified. - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; -} - -std::unique_ptr<GroupFromFirstDocumentTransformation> GroupFromFirstDocumentTransformation::create( - const intrusive_ptr<ExpressionContext>& expCtx, - const std::string& groupId, - vector<pair<std::string, intrusive_ptr<Expression>>> accumulatorExprs) { - return std::make_unique<GroupFromFirstDocumentTransformation>(groupId, - std::move(accumulatorExprs)); -} - constexpr StringData DocumentSourceGroup::kStageName; REGISTER_DOCUMENT_SOURCE(group, @@ -132,449 +56,70 @@ const char* DocumentSourceGroup::getSourceName() const { return kStageName.rawData(); } -bool DocumentSourceGroup::shouldSpillWithAttemptToSaveMemory() { - if (!_memoryTracker._allowDiskUse && - (_memoryTracker.currentMemoryBytes() > - static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes))) { - freeMemory(); - } - - if (_memoryTracker.currentMemoryBytes() > - static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes)) { - uassert(ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed, - "Exceeded memory limit for $group, but didn't allow external sort." - " Pass allowDiskUse:true to opt in.", - _memoryTracker._allowDiskUse); - _memoryTracker.resetCurrent(); - return true; - } - return false; -} - -void DocumentSourceGroup::freeMemory() { - invariant(_groups); - for (auto&& group : *_groups) { - for (size_t i = 0; i < group.second.size(); i++) { - // Subtract the current usage. - _memoryTracker.update(_accumulatedFields[i].fieldName, - -1 * group.second[i]->getMemUsage()); - - group.second[i]->reduceMemoryConsumptionIfAble(); - - // Update the memory usage for this AccumulationStatement. - _memoryTracker.update(_accumulatedFields[i].fieldName, group.second[i]->getMemUsage()); - } - } -} - -DocumentSource::GetNextResult DocumentSourceGroup::doGetNext() { - if (!_initialized) { - const auto initializationResult = initialize(); - if (initializationResult.isPaused()) { - return initializationResult; - } - invariant(initializationResult.isEOF()); - } - - for (auto&& accum : _currentAccumulators) { - accum->reset(); // Prep accumulators for a new group. - } - - if (_spilled) { - return getNextSpilled(); - } else { - return getNextStandard(); - } -} - -DocumentSource::GetNextResult DocumentSourceGroup::getNextSpilled() { - // We aren't streaming, and we have spilled to disk. - if (!_sorterIterator) - return GetNextResult::makeEOF(); - - _currentId = _firstPartOfNextGroup.first; - const size_t numAccumulators = _accumulatedFields.size(); - - // Call startNewGroup on every accumulator. - Value expandedId = expandId(_currentId); - Document idDoc = - expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); - for (size_t i = 0; i < numAccumulators; ++i) { - Value initializerValue = - _accumulatedFields[i].expr.initializer->evaluate(idDoc, &pExpCtx->variables); - _currentAccumulators[i]->startNewGroup(initializerValue); - } - - while (pExpCtx->getValueComparator().evaluate(_currentId == _firstPartOfNextGroup.first)) { - // Inside of this loop, _firstPartOfNextGroup is the current data being processed. - // At loop exit, it is the first value to be processed in the next group. - switch (numAccumulators) { // mirrors switch in spill() - case 1: // Single accumulators serialize as a single Value. - _currentAccumulators[0]->process(_firstPartOfNextGroup.second, true); - case 0: // No accumulators so no Values. - break; - default: { // Multiple accumulators serialize as an array of Values. - const vector<Value>& accumulatorStates = _firstPartOfNextGroup.second.getArray(); - for (size_t i = 0; i < numAccumulators; i++) { - _currentAccumulators[i]->process(accumulatorStates[i], true); - } - } - } - - if (!_sorterIterator->more()) { - dispose(); - break; - } - - _firstPartOfNextGroup = _sorterIterator->next(); - } - - return makeDocument(_currentId, _currentAccumulators, pExpCtx->needsMerge); -} - -DocumentSource::GetNextResult DocumentSourceGroup::getNextStandard() { - // Not spilled, and not streaming. - if (_groups->empty()) - return GetNextResult::makeEOF(); - - Document out = makeDocument(groupsIterator->first, groupsIterator->second, pExpCtx->needsMerge); - - if (++groupsIterator == _groups->end()) - dispose(); - - return out; -} - -void DocumentSourceGroup::doDispose() { - // Free our resources. - _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); - _sorterIterator.reset(); - - // Make us look done. - groupsIterator = _groups->end(); -} - -intrusive_ptr<DocumentSource> DocumentSourceGroup::optimize() { - // Optimizing a 'DocumentSourceGroup' might modify its expressions to become incompatible with - // SBE. We temporarily highjack the context's 'sbeCompatible' flag to communicate the situation - // back to the 'DocumentSourceGroup'. Notice, that while a particular 'DocumentSourceGroup' - // might become incompatible with SBE, other groups in the pipeline and the collection access - // could be still eligible for lowering to SBE, thus we must reset the context's 'sbeCompatible' - // flag back to its original value at the end of the 'optimize()' call. - // - // TODO SERVER-XXXXX: replace this hack with a proper per-stage tracking of SBE compatibility. - auto expCtx = _idExpressions[0]->getExpressionContext(); - auto orgSbeCompatible = expCtx->sbeCompatible; - expCtx->sbeCompatible = true; - - // TODO: If all _idExpressions are ExpressionConstants after optimization, then we know there - // will be only one group. We should take advantage of that to avoid going through the hash - // table. - for (size_t i = 0; i < _idExpressions.size(); i++) { - _idExpressions[i] = _idExpressions[i]->optimize(); - } - - for (auto&& accumulatedField : _accumulatedFields) { - accumulatedField.expr.initializer = accumulatedField.expr.initializer->optimize(); - accumulatedField.expr.argument = accumulatedField.expr.argument->optimize(); - } - - _sbeCompatible = _sbeCompatible && expCtx->sbeCompatible; - expCtx->sbeCompatible = orgSbeCompatible; - - return this; -} - -Value DocumentSourceGroup::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { - MutableDocument insides; - - // Add the _id. - if (_idFieldNames.empty()) { - invariant(_idExpressions.size() == 1); - insides["_id"] = _idExpressions[0]->serialize(static_cast<bool>(explain)); - } else { - // Decomposed document case. - invariant(_idExpressions.size() == _idFieldNames.size()); - MutableDocument md; - for (size_t i = 0; i < _idExpressions.size(); i++) { - md[_idFieldNames[i]] = _idExpressions[i]->serialize(static_cast<bool>(explain)); - } - insides["_id"] = md.freezeToValue(); - } - - // Add the remaining fields. - for (auto&& accumulatedField : _accumulatedFields) { - intrusive_ptr<AccumulatorState> accum = accumulatedField.makeAccumulator(); - insides[accumulatedField.fieldName] = - Value(accum->serialize(accumulatedField.expr.initializer, - accumulatedField.expr.argument, - static_cast<bool>(explain))); - } - - if (_doingMerge) { - // This makes the output unparsable (with error) on pre 2.6 shards, but it will never - // be sent to old shards when this flag is true since they can't do a merge anyway. - insides["$doingMerge"] = Value(true); - } - - MutableDocument out; - out[getSourceName()] = Value(insides.freeze()); - - if (explain && *explain >= ExplainOptions::Verbosity::kExecStats) { - MutableDocument md; - - for (size_t i = 0; i < _accumulatedFields.size(); i++) { - md[_accumulatedFields[i].fieldName] = Value(static_cast<long long>( - _memoryTracker[_accumulatedFields[i].fieldName].maxMemoryBytes())); - } - - out["maxAccumulatorMemoryUsageBytes"] = Value(md.freezeToValue()); - out["totalOutputDataSizeBytes"] = - Value(static_cast<long long>(_stats.totalOutputDataSizeBytes)); - out["usedDisk"] = Value(_stats.spills > 0); - out["spills"] = Value(static_cast<long long>(_stats.spills)); - } - - return Value(out.freezeToValue()); -} - -DepsTracker::State DocumentSourceGroup::getDependencies(DepsTracker* deps) const { - // add the _id - for (size_t i = 0; i < _idExpressions.size(); i++) { - _idExpressions[i]->addDependencies(deps); - } - - // add the rest - for (auto&& accumulatedField : _accumulatedFields) { - accumulatedField.expr.argument->addDependencies(deps); - // Don't add initializer, because it doesn't refer to docs from the input stream. - } - - return DepsTracker::State::EXHAUSTIVE_ALL; -} - -DocumentSource::GetModPathsReturn DocumentSourceGroup::getModifiedPaths() const { - // We preserve none of the fields, but any fields referenced as part of the group key are - // logically just renamed. - StringMap<std::string> renames; - for (std::size_t i = 0; i < _idExpressions.size(); ++i) { - auto idExp = _idExpressions[i]; - auto pathToPutResultOfExpression = - _idFieldNames.empty() ? "_id" : "_id." + _idFieldNames[i]; - auto computedPaths = idExp->getComputedPaths(pathToPutResultOfExpression); - for (auto&& rename : computedPaths.renames) { - renames[rename.first] = rename.second; - } - } - - return {DocumentSource::GetModPathsReturn::Type::kAllExcept, - std::set<std::string>{}, // No fields are preserved. - std::move(renames)}; -} - -StringMap<boost::intrusive_ptr<Expression>> DocumentSourceGroup::getIdFields() const { - if (_idFieldNames.empty()) { - invariant(_idExpressions.size() == 1); - return {{"_id", _idExpressions[0]}}; - } else { - invariant(_idFieldNames.size() == _idExpressions.size()); - StringMap<boost::intrusive_ptr<Expression>> result; - for (std::size_t i = 0; i < _idFieldNames.size(); ++i) { - result["_id." + _idFieldNames[i]] = _idExpressions[i]; - } - return result; - } -} - -const std::vector<AccumulationStatement>& DocumentSourceGroup::getAccumulatedFields() const { - return _accumulatedFields; -} - -intrusive_ptr<DocumentSourceGroup> DocumentSourceGroup::create( - const intrusive_ptr<ExpressionContext>& expCtx, +boost::intrusive_ptr<DocumentSourceGroup> DocumentSourceGroup::create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, const boost::intrusive_ptr<Expression>& groupByExpression, std::vector<AccumulationStatement> accumulationStatements, boost::optional<size_t> maxMemoryUsageBytes) { - intrusive_ptr<DocumentSourceGroup> groupStage( - new DocumentSourceGroup(expCtx, maxMemoryUsageBytes)); + boost::intrusive_ptr<DocumentSourceGroup> groupStage = + new DocumentSourceGroup(expCtx, maxMemoryUsageBytes); groupStage->setIdExpression(groupByExpression); for (auto&& statement : accumulationStatements) { groupStage->addAccumulator(statement); - groupStage->_memoryTracker.set(statement.fieldName, 0); } return groupStage; } -DocumentSourceGroup::DocumentSourceGroup(const intrusive_ptr<ExpressionContext>& expCtx, +DocumentSourceGroup::DocumentSourceGroup(const boost::intrusive_ptr<ExpressionContext>& expCtx, boost::optional<size_t> maxMemoryUsageBytes) - : DocumentSource(kStageName, expCtx), - _doingMerge(false), - _memoryTracker{expCtx->allowDiskUse && !expCtx->inMongos, - maxMemoryUsageBytes - ? *maxMemoryUsageBytes - : static_cast<size_t>(internalDocumentSourceGroupMaxMemoryBytes.load())}, - _initialized(false), - _groups(expCtx->getValueComparator().makeUnorderedValueMap<Accumulators>()), - _spilled(false), - _sbeCompatible(false) {} + : DocumentSourceGroupBase(kStageName, expCtx, maxMemoryUsageBytes), _groupsReady(false) {} -void DocumentSourceGroup::addAccumulator(AccumulationStatement accumulationStatement) { - _accumulatedFields.push_back(accumulationStatement); +boost::intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBson( + BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { + return createFromBsonWithMaxMemoryUsage(std::move(elem), expCtx, boost::none); } -namespace { - -intrusive_ptr<Expression> parseIdExpression(const intrusive_ptr<ExpressionContext>& expCtx, - BSONElement groupField, - const VariablesParseState& vps) { - if (groupField.type() == Object) { - // {_id: {}} is treated as grouping on a constant, not an expression - if (groupField.Obj().isEmpty()) { - return ExpressionConstant::create(expCtx.get(), Value(groupField)); - } - - const BSONObj idKeyObj = groupField.Obj(); - if (idKeyObj.firstElementFieldName()[0] == '$') { - // grouping on a $op expression - return Expression::parseObject(expCtx.get(), idKeyObj, vps); - } else { - for (auto&& field : idKeyObj) { - uassert(17390, - "$group does not support inclusion-style expressions", - !field.isNumber() && field.type() != Bool); - } - return ExpressionObject::parse(expCtx.get(), idKeyObj, vps); - } - } else { - return Expression::parseOperand(expCtx.get(), groupField, vps); - } +boost::intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBsonWithMaxMemoryUsage( + BSONElement elem, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes) { + boost::intrusive_ptr<DocumentSourceGroup> groupStage( + new DocumentSourceGroup(expCtx, maxMemoryUsageBytes)); + groupStage->initializeFromBson(elem); + return groupStage; } -} // namespace - -void DocumentSourceGroup::setIdExpression(const boost::intrusive_ptr<Expression> idExpression) { - if (auto object = dynamic_cast<ExpressionObject*>(idExpression.get())) { - auto& childExpressions = object->getChildExpressions(); - invariant(!childExpressions.empty()); // We expect to have converted an empty object into a - // constant expression. - - // grouping on an "artificial" object. Rather than create the object for each input - // in initialize(), instead group on the output of the raw expressions. The artificial - // object will be created at the end in makeDocument() while outputting results. - for (auto&& childExpPair : childExpressions) { - _idFieldNames.push_back(childExpPair.first); - _idExpressions.push_back(childExpPair.second); +DocumentSource::GetNextResult DocumentSourceGroup::doGetNext() { + if (!_groupsReady) { + const auto initializationResult = performBlockingGroup(); + if (initializationResult.isPaused()) { + return initializationResult; } - } else { - _idExpressions.push_back(idExpression); - } -} - -boost::intrusive_ptr<Expression> DocumentSourceGroup::getIdExpression() const { - // _idFieldNames is empty and _idExpressions has one element when the _id expression is not an - // object expression. - if (_idFieldNames.empty() && _idExpressions.size() == 1) { - return _idExpressions[0]; - } - - tassert(6586300, - "Field and its expression must be always paired in ExpressionObject", - _idFieldNames.size() > 0 && _idFieldNames.size() == _idExpressions.size()); - - // Each expression in '_idExpressions' may have been optimized and so, compose the object _id - // expression out of the optimized expressions. - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fieldsAndExprs; - for (size_t i = 0; i < _idExpressions.size(); ++i) { - fieldsAndExprs.emplace_back(_idFieldNames[i], _idExpressions[i]); + invariant(initializationResult.isEOF()); } - return ExpressionObject::create(_idExpressions[0]->getExpressionContext(), - std::move(fieldsAndExprs)); -} - -intrusive_ptr<DocumentSource> DocumentSourceGroup::createFromBson( - BSONElement elem, const intrusive_ptr<ExpressionContext>& expCtx) { - uassert(15947, "a group's fields must be specified in an object", elem.type() == Object); - - intrusive_ptr<DocumentSourceGroup> groupStage(new DocumentSourceGroup(expCtx)); - - BSONObj groupObj(elem.Obj()); - BSONObjIterator groupIterator(groupObj); - VariablesParseState vps = expCtx->variablesParseState; - expCtx->sbeGroupCompatible = true; - while (groupIterator.more()) { - BSONElement groupField(groupIterator.next()); - StringData pFieldName = groupField.fieldNameStringData(); - if (pFieldName == "_id") { - uassert(15948, - "a group's _id may only be specified once", - groupStage->_idExpressions.empty()); - groupStage->setIdExpression(parseIdExpression(expCtx, groupField, vps)); - invariant(!groupStage->_idExpressions.empty()); - } else if (pFieldName == "$doingMerge") { - massert(17030, "$doingMerge should be true if present", groupField.Bool()); - - groupStage->setDoingMerge(true); - } else { - // Any other field will be treated as an accumulator specification. - groupStage->addAccumulator( - AccumulationStatement::parseAccumulationStatement(expCtx.get(), groupField, vps)); - groupStage->_memoryTracker.set(pFieldName, 0); - } + auto result = getNextReadyGroup(); + if (result.isEOF()) { + dispose(); } - groupStage->_sbeCompatible = expCtx->sbeGroupCompatible && expCtx->sbeCompatible; - - uassert( - 15955, "a group specification must include an _id", !groupStage->_idExpressions.empty()); - return groupStage; + return result; } -namespace { - -using GroupsMap = DocumentSourceGroup::GroupsMap; - -class SorterComparator { -public: - typedef pair<Value, Value> Data; - - SorterComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} - - int operator()(const Data& lhs, const Data& rhs) const { - return _valueComparator.compare(lhs.first, rhs.first); - } - -private: - ValueComparator _valueComparator; -}; - -class SpillSTLComparator { -public: - SpillSTLComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} - - bool operator()(const GroupsMap::value_type* lhs, const GroupsMap::value_type* rhs) const { - return _valueComparator.evaluate(lhs->first < rhs->first); - } - -private: - ValueComparator _valueComparator; -}; -} // namespace - -DocumentSource::GetNextResult DocumentSourceGroup::initialize() { +DocumentSource::GetNextResult DocumentSourceGroup::performBlockingGroup() { GetNextResult input = pSource->getNext(); - return initializeSelf(input); + return performBlockingGroupSelf(input); } -// This separate NOINLINE function is used here to decrease stack utilization of initialize() and -// prevent stack overflows. -MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initializeSelf( +// This separate NOINLINE function is used here to decrease stack utilization of +// performBlockingGroup() and prevent stack overflows. +MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::performBlockingGroupSelf( GetNextResult input) { - const size_t numAccumulators = _accumulatedFields.size(); + setExecutionStarted(); // Barring any pausing, this loop exhausts 'pSource' and populates '_groups'. for (; input.isAdvanced(); input = pSource->getNext()) { if (shouldSpillWithAttemptToSaveMemory()) { - _sortedFiles.push_back(spill()); + spill(); } // We release the result document here so that it does not outlive the end of this loop @@ -582,58 +127,7 @@ MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initi auto rootDocument = input.releaseDocument(); Value id = computeId(rootDocument); - // Look for the _id value in the map. If it's not there, add a new entry with a blank - // accumulator. This is done in a somewhat odd way in order to avoid hashing 'id' and - // looking it up in '_groups' multiple times. - const size_t oldSize = _groups->size(); - vector<intrusive_ptr<AccumulatorState>>& group = (*_groups)[id]; - const bool inserted = _groups->size() != oldSize; - - vector<uint64_t> oldAccumMemUsage(numAccumulators, 0); - if (inserted) { - _memoryTracker.set(_memoryTracker.currentMemoryBytes() + id.getApproximateSize()); - - // Initialize and add the accumulators - Value expandedId = expandId(id); - Document idDoc = - expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); - group.reserve(numAccumulators); - for (auto&& accumulatedField : _accumulatedFields) { - auto accum = accumulatedField.makeAccumulator(); - Value initializerValue = - accumulatedField.expr.initializer->evaluate(idDoc, &pExpCtx->variables); - accum->startNewGroup(initializerValue); - group.push_back(accum); - } - } - - /* tickle all the accumulators for the group we found */ - dassert(numAccumulators == group.size()); - - for (size_t i = 0; i < numAccumulators; i++) { - // Only process the input and update the memory footprint if the current accumulator - // needs more input. - if (group[i]->needsInput()) { - const auto prevMemUsage = inserted ? 0 : group[i]->getMemUsage(); - group[i]->process(_accumulatedFields[i].expr.argument->evaluate( - rootDocument, &pExpCtx->variables), - _doingMerge); - _memoryTracker.update(_accumulatedFields[i].fieldName, - group[i]->getMemUsage() - prevMemUsage); - } - } - - if (kDebugBuild && !storageGlobalParams.readOnly) { - // In debug mode, spill every time we have a duplicate id to stress merge logic. - if (!inserted && // is a dup - !pExpCtx->inMongos && // can't spill to disk in mongos - !_memoryTracker - ._allowDiskUse && // don't change behavior when testing external sort - _sortedFiles.size() < 20) { // don't open too many FDs - - _sortedFiles.push_back(spill()); - } - } + processDocument(id, rootDocument); } switch (input.getStatus()) { @@ -644,280 +138,14 @@ MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult DocumentSourceGroup::initi return input; // Propagate pause. } case DocumentSource::GetNextResult::ReturnStatus::kEOF: { - // Do any final steps necessary to prepare to output results. - if (!_sortedFiles.empty()) { - _spilled = true; - if (!_groups->empty()) { - _sortedFiles.push_back(spill()); - } - - // We won't be using groups again so free its memory. - _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); - - _sorterIterator.reset(Sorter<Value, Value>::Iterator::merge( - _sortedFiles, SortOptions(), SorterComparator(pExpCtx->getValueComparator()))); - - // prepare current to accumulate data - _currentAccumulators.reserve(numAccumulators); - for (auto&& accumulatedField : _accumulatedFields) { - _currentAccumulators.push_back(accumulatedField.makeAccumulator()); - } - - verify(_sorterIterator->more()); // we put data in, we should get something out. - _firstPartOfNextGroup = _sorterIterator->next(); - } else { - // start the group iterator - groupsIterator = _groups->begin(); - } - + readyGroups(); // This must happen last so that, unless control gets here, we will re-enter // initialization after getting a GetNextResult::ResultState::kPauseExecution. - _initialized = true; + _groupsReady = true; return input; } } MONGO_UNREACHABLE; } -shared_ptr<Sorter<Value, Value>::Iterator> DocumentSourceGroup::spill() { - _stats.spills++; - - vector<const GroupsMap::value_type*> ptrs; // using pointers to speed sorting - ptrs.reserve(_groups->size()); - for (GroupsMap::const_iterator it = _groups->begin(), end = _groups->end(); it != end; ++it) { - ptrs.push_back(&*it); - } - - stable_sort(ptrs.begin(), ptrs.end(), SpillSTLComparator(pExpCtx->getValueComparator())); - - // Initialize '_file' in a lazy manner only when it is needed. - if (!_file) { - _file = - std::make_shared<Sorter<Value, Value>::File>(pExpCtx->tempDir + "/" + nextFileName()); - } - SortedFileWriter<Value, Value> writer(SortOptions().TempDir(pExpCtx->tempDir), _file); - switch (_accumulatedFields.size()) { // same as ptrs[i]->second.size() for all i. - case 0: // no values, essentially a distinct - for (size_t i = 0; i < ptrs.size(); i++) { - writer.addAlreadySorted(ptrs[i]->first, Value()); - } - break; - - case 1: // just one value, use optimized serialization as single Value - for (size_t i = 0; i < ptrs.size(); i++) { - writer.addAlreadySorted(ptrs[i]->first, - ptrs[i]->second[0]->getValue(/*toBeMerged=*/true)); - } - break; - - default: // multiple values, serialize as array-typed Value - for (size_t i = 0; i < ptrs.size(); i++) { - vector<Value> accums; - for (size_t j = 0; j < ptrs[i]->second.size(); j++) { - accums.push_back(ptrs[i]->second[j]->getValue(/*toBeMerged=*/true)); - } - writer.addAlreadySorted(ptrs[i]->first, Value(std::move(accums))); - } - break; - } - - auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx); - metricsCollector.incrementKeysSorted(ptrs.size()); - metricsCollector.incrementSorterSpills(1); - - _groups->clear(); - // Zero out the current per-accumulation statement memory consumption, as the memory has been - // freed by spilling. - for (auto accum : _accumulatedFields) { - _memoryTracker.set(accum.fieldName, 0); - } - - Sorter<Value, Value>::Iterator* iteratorPtr = writer.done(); - return shared_ptr<Sorter<Value, Value>::Iterator>(iteratorPtr); -} - -Value DocumentSourceGroup::computeId(const Document& root) { - // If only one expression, return result directly - if (_idExpressions.size() == 1) { - Value retValue = _idExpressions[0]->evaluate(root, &pExpCtx->variables); - return retValue.missing() ? Value(BSONNULL) : std::move(retValue); - } - - // Multiple expressions get results wrapped in a vector - vector<Value> vals; - vals.reserve(_idExpressions.size()); - for (size_t i = 0; i < _idExpressions.size(); i++) { - vals.push_back(_idExpressions[i]->evaluate(root, &pExpCtx->variables)); - } - return Value(std::move(vals)); -} - -Value DocumentSourceGroup::expandId(const Value& val) { - // _id doesn't get wrapped in a document - if (_idFieldNames.empty()) - return val; - - // _id is a single-field document containing val - if (_idFieldNames.size() == 1) - return Value(DOC(_idFieldNames[0] << val)); - - // _id is a multi-field document containing the elements of val - const vector<Value>& vals = val.getArray(); - invariant(_idFieldNames.size() == vals.size()); - MutableDocument md(vals.size()); - for (size_t i = 0; i < vals.size(); i++) { - md[_idFieldNames[i]] = vals[i]; - } - return md.freezeToValue(); -} - -Document DocumentSourceGroup::makeDocument(const Value& id, - const Accumulators& accums, - bool mergeableOutput) { - const size_t n = _accumulatedFields.size(); - MutableDocument out(1 + n); - - /* add the _id field */ - out.addField("_id", expandId(id)); - - /* add the rest of the fields */ - for (size_t i = 0; i < n; ++i) { - Value val = accums[i]->getValue(mergeableOutput); - if (val.missing()) { - // we return null in this case so return objects are predictable - out.addField(_accumulatedFields[i].fieldName, Value(BSONNULL)); - } else { - out.addField(_accumulatedFields[i].fieldName, std::move(val)); - } - } - - _stats.totalOutputDataSizeBytes += out.getApproximateSize(); - return out.freeze(); -} - -boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceGroup::distributedPlanLogic() { - intrusive_ptr<DocumentSourceGroup> mergingGroup(new DocumentSourceGroup(pExpCtx)); - mergingGroup->setDoingMerge(true); - - VariablesParseState vps = pExpCtx->variablesParseState; - /* the merger will use the same grouping key */ - mergingGroup->setIdExpression(ExpressionFieldPath::parse(pExpCtx.get(), "$$ROOT._id", vps)); - - for (auto&& accumulatedField : _accumulatedFields) { - // The merger's output field names will be the same, as will the accumulator factories. - // However, for some accumulators, the expression to be accumulated will be different. The - // original accumulator may be collecting an expression based on a field expression or - // constant. Here, we accumulate the output of the same name from the prior group. - auto copiedAccumulatedField = accumulatedField; - copiedAccumulatedField.expr.argument = ExpressionFieldPath::parse( - pExpCtx.get(), "$$ROOT." + copiedAccumulatedField.fieldName, vps); - mergingGroup->addAccumulator(copiedAccumulatedField); - mergingGroup->_memoryTracker.set(copiedAccumulatedField.fieldName, 0); - } - - // {shardsStage, mergingStage, sortPattern} - return DistributedPlanLogic{this, mergingGroup, boost::none}; -} - -bool DocumentSourceGroup::pathIncludedInGroupKeys(const std::string& dottedPath) const { - return std::any_of( - _idExpressions.begin(), _idExpressions.end(), [&dottedPath](const auto& exp) { - if (auto fieldExp = dynamic_cast<ExpressionFieldPath*>(exp.get())) { - if (fieldExp->representsPath(dottedPath)) { - return true; - } - } - return false; - }); -} - -bool DocumentSourceGroup::canRunInParallelBeforeWriteStage( - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const { - if (_doingMerge) { - return true; // This is fine. - } - - // Certain $group stages are allowed to execute on each exchange consumer. In order to - // guarantee each consumer will only group together data from its own shard, the $group must - // group on a superset of the shard key. - for (auto&& currentPathOfShardKey : nameOfShardKeyFieldsUponEntryToStage) { - if (!pathIncludedInGroupKeys(currentPathOfShardKey)) { - // This requires an exact path match, but as a future optimization certain path - // prefixes should be okay. For example, if the shard key path is "a.b", and we're - // grouping by "a", then each group of "a" is strictly more specific than "a.b", so - // we can deduce that grouping by "a" will not need to group together documents - // across different values of the shard key field "a.b", and thus as long as any - // other shard key fields are similarly preserved will not need to consume a merged - // stream to perform the group. - return false; - } - } - return true; -} - -std::unique_ptr<GroupFromFirstDocumentTransformation> -DocumentSourceGroup::rewriteGroupAsTransformOnFirstDocument() const { - if (_idExpressions.size() != 1) { - // This transformation is only intended for $group stages that group on a single field. - return nullptr; - } - - auto fieldPathExpr = dynamic_cast<ExpressionFieldPath*>(_idExpressions.front().get()); - if (!fieldPathExpr || fieldPathExpr->isVariableReference()) { - return nullptr; - } - - const auto fieldPath = fieldPathExpr->getFieldPath(); - if (fieldPath.getPathLength() == 1) { - // The path is $$CURRENT or $$ROOT. This isn't really a sensible value to group by (since - // each document has a unique _id, it will just return the entire collection). We only - // apply the rewrite when grouping by a single field, so we cannot apply it in this case, - // where we are grouping by the entire document. - tassert(5943200, - "Optimization attempted on group by always-dissimilar system variable", - fieldPath.getFieldName(0) == "CURRENT" || fieldPath.getFieldName(0) == "ROOT"); - return nullptr; - } - - const auto groupId = fieldPath.tail().fullPath(); - - // We can't do this transformation if there are any non-$first accumulators. - for (auto&& accumulator : _accumulatedFields) { - if (AccumulatorDocumentsNeeded::kFirstDocument != - accumulator.makeAccumulator()->documentsNeeded()) { - return nullptr; - } - } - - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fields; - - boost::intrusive_ptr<Expression> idField; - // The _id field can be specified either as a fieldpath (ex. _id: "$a") or as a singleton - // object (ex. _id: {v: "$a"}). - if (_idFieldNames.empty()) { - idField = ExpressionFieldPath::deprecatedCreate(pExpCtx.get(), groupId); - } else { - invariant(_idFieldNames.size() == 1); - idField = ExpressionObject::create(pExpCtx.get(), - {{_idFieldNames.front(), _idExpressions.front()}}); - } - fields.push_back(std::make_pair("_id", idField)); - - for (auto&& accumulator : _accumulatedFields) { - fields.push_back(std::make_pair(accumulator.fieldName, accumulator.expr.argument)); - - // Since we don't attempt this transformation for non-$first accumulators, - // the initializer should always be trivial. - } - - return GroupFromFirstDocumentTransformation::create(pExpCtx, groupId, std::move(fields)); -} - -size_t DocumentSourceGroup::getMaxMemoryUsageBytes() const { - return _memoryTracker._maxAllowedMemoryUsageBytes; -} - } // namespace mongo - -#include "mongo/db/sorter/sorter.cpp" -// Explicit instantiation unneeded since we aren't exposing Sorter outside of this file. diff --git a/src/mongo/db/pipeline/document_source_group.h b/src/mongo/db/pipeline/document_source_group.h index edffed6758b..a1f8e9b2b9a 100644 --- a/src/mongo/db/pipeline/document_source_group.h +++ b/src/mongo/db/pipeline/document_source_group.h @@ -32,75 +32,19 @@ #include <memory> #include <utility> -#include "mongo/db/pipeline/accumulation_statement.h" -#include "mongo/db/pipeline/accumulator.h" -#include "mongo/db/pipeline/document_source.h" -#include "mongo/db/pipeline/memory_usage_tracker.h" -#include "mongo/db/pipeline/transformer_interface.h" -#include "mongo/db/sorter/sorter.h" +#include "mongo/db/pipeline/document_source_group_base.h" namespace mongo { /** - * GroupFromFirstTransformation consists of a list of (field name, expression pairs). It returns a - * document synthesized by assigning each field name in the output document to the result of - * evaluating the corresponding expression. If the expression evaluates to missing, we assign a - * value of BSONNULL. This is necessary to match the semantics of $first for missing fields. + * This class represents hash based group implementation that stores all groups until source is + * depleted and only then starts outputing documents. */ -class GroupFromFirstDocumentTransformation final : public TransformerInterface { +class DocumentSourceGroup final : public DocumentSourceGroupBase { public: - GroupFromFirstDocumentTransformation( - const std::string& groupId, - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs) - : _accumulatorExprs(std::move(accumulatorExprs)), _groupId(groupId) {} - - TransformerType getType() const final { - return TransformerType::kGroupFromFirstDocument; - } - - /** - * The path of the field that we are grouping on: i.e., the field in the input document that we - * will use to create the _id field of the ouptut document. - */ - const std::string& groupId() const { - return _groupId; - } - - Document applyTransformation(const Document& input) final; - - void optimize() final; - - Document serializeTransformation( - boost::optional<ExplainOptions::Verbosity> explain) const final; - - DepsTracker::State addDependencies(DepsTracker* deps) const final; - - DocumentSource::GetModPathsReturn getModifiedPaths() const final; - - static std::unique_ptr<GroupFromFirstDocumentTransformation> create( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const std::string& groupId, - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs); - -private: - std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> _accumulatorExprs; - std::string _groupId; -}; - -class DocumentSourceGroup final : public DocumentSource { -public: - using Accumulators = std::vector<boost::intrusive_ptr<AccumulatorState>>; - using GroupsMap = ValueUnorderedMap<Accumulators>; - static constexpr StringData kStageName = "$group"_sd; - boost::intrusive_ptr<DocumentSource> optimize() final; - DepsTracker::State getDependencies(DepsTracker* deps) const final; - Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; const char* getSourceName() const final; - GetModPathsReturn getModifiedPaths() const final; - StringMap<boost::intrusive_ptr<Expression>> getIdFields() const; - const std::vector<AccumulationStatement>& getAccumulatedFields() const; /** * Convenience method for creating a new $group stage. If maxMemoryUsageBytes is boost::none, @@ -118,199 +62,40 @@ public: */ static boost::intrusive_ptr<DocumentSource> createFromBson( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); - - StageConstraints constraints(Pipeline::SplitState pipeState) const final { - StageConstraints constraints(StreamType::kBlocking, - PositionRequirement::kNone, - HostTypeRequirement::kNone, - DiskUseRequirement::kWritesTmpData, - FacetRequirement::kAllowed, - TransactionRequirement::kAllowed, - LookupRequirement::kAllowed, - UnionRequirement::kAllowed); - constraints.canSwapWithMatch = true; - return constraints; - } - - /** - * Add an accumulator, which will become a field in each Document that results from grouping. - */ - void addAccumulator(AccumulationStatement accumulationStatement); - - /** - * Sets the expression to use to determine the group id of each document. - */ - void setIdExpression(boost::intrusive_ptr<Expression> idExpression); - - /** - * Returns the expression to use to determine the group id of each document. - */ - boost::intrusive_ptr<Expression> getIdExpression() const; - - /** - * Returns true if this $group stage represents a 'global' $group which is merging together - * results from earlier partial groups. - */ - bool doingMerge() const { - return _doingMerge; - } - - /** - * Tell this source if it is doing a merge from shards. Defaults to false. - */ - void setDoingMerge(bool doingMerge) { - _doingMerge = doingMerge; - } - - /** - * Returns true if this $group stage used disk during execution and false otherwise. - */ - bool usedDisk() final { - return _stats.spills > 0; - } - - const SpecificStats* getSpecificStats() const final { - return &_stats; - } - - boost::optional<DistributedPlanLogic> distributedPlanLogic() final; - bool canRunInParallelBeforeWriteStage( - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const final; - - /** - * When possible, creates a document transformer that transforms the first document in a group - * into one of the output documents of the $group stage. This is possible when we are grouping - * on a single field and all accumulators are $first (or there are no accumluators). - * - * It is sometimes possible to use a DISTINCT_SCAN to scan the first document of each group, - * in which case this transformation can replace the actual $group stage in the pipeline - * (SERVER-9507). - */ - std::unique_ptr<GroupFromFirstDocumentTransformation> rewriteGroupAsTransformOnFirstDocument() - const; - - /** - * Returns maximum allowed memory footprint. - */ - size_t getMaxMemoryUsageBytes() const; - - // True if this $group can be pushed down to SBE. - bool sbeCompatible() const { - return _sbeCompatible; - } + static boost::intrusive_ptr<DocumentSource> createFromBsonWithMaxMemoryUsage( + BSONElement elem, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes); protected: GetNextResult doGetNext() final; - void doDispose() final; + + bool isSpecFieldReserved(StringData) final { + return false; + } private: explicit DocumentSourceGroup(const boost::intrusive_ptr<ExpressionContext>& expCtx, boost::optional<size_t> maxMemoryUsageBytes = boost::none); /** - * getNext() dispatches to one of these three depending on what type of $group it is. These - * methods expect '_currentAccumulators' to have been reset before being called, and also expect - * initialize() to have been called already. - */ - GetNextResult getNextSpilled(); - GetNextResult getNextStandard(); - - /** - * Before returning anything, this source must prepare itself. In a streaming $group, - * initialize() requests the first document from the previous source, and uses it to prepare the - * accumulators. In an unsorted $group, initialize() exhausts the previous source before - * returning. The '_initialized' boolean indicates that initialize() has finished. + * Before returning anything, this source must prepare itself. performBlockingGroup() exhausts + * the previous source before + * returning. The '_groupsReady' boolean indicates that performBlockingGroup() has finished. * * This method may not be able to finish initialization in a single call if 'pSource' returns a * DocumentSource::GetNextResult::kPauseExecution, so it returns the last GetNextResult * encountered, which may be either kEOF or kPauseExecution. */ - GetNextResult initialize(); + GetNextResult performBlockingGroup(); /** - * Initializes this $group after any children are potentially initialized see initialize() for + * Initializes this $group after any children are initialized. See performBlockingGroup() for * more details. */ - GetNextResult initializeSelf(GetNextResult input); - - /** - * Spill groups map to disk and returns an iterator to the file. Note: Since a sorted $group - * does not exhaust the previous stage before returning, and thus does not maintain as large a - * store of documents at any one time, only an unsorted group can spill to disk. - */ - std::shared_ptr<Sorter<Value, Value>::Iterator> spill(); - - /** - * If we ran out of memory, finish all the pending operations so that some memory - * can be freed. - */ - void freeMemory(); - - Document makeDocument(const Value& id, const Accumulators& accums, bool mergeableOutput); - - /** - * Computes the internal representation of the group key. - */ - Value computeId(const Document& root); - - /** - * Converts the internal representation of the group key to the _id shape specified by the - * user. - */ - Value expandId(const Value& val); - - /** - * Returns true if 'dottedPath' is one of the group keys present in '_idExpressions'. - */ - bool pathIncludedInGroupKeys(const std::string& dottedPath) const; - - /** - * Cleans up any pending memory usage. Throws error, if memory usage is above - * 'maxMemoryUsageBytes' and cannot spill to disk. - * - * Returns true, if the caller should spill to disk, false otherwise. - */ - bool shouldSpillWithAttemptToSaveMemory(); - - std::vector<AccumulationStatement> _accumulatedFields; - - bool _doingMerge; - - MemoryUsageTracker _memoryTracker; - - GroupStats _stats; - - std::shared_ptr<Sorter<Value, Value>::File> _file; - - // If the expression for the '_id' field represents a non-empty object, we track its fields' - // names in '_idFieldNames'. - std::vector<std::string> _idFieldNames; - // Expressions for the individual fields when '_id' produces a document in the order of - // '_idFieldNames' or the whole expression otherwise. - std::vector<boost::intrusive_ptr<Expression>> _idExpressions; - - bool _initialized; - - Value _currentId; - Accumulators _currentAccumulators; - - // We use boost::optional to defer initialization until the ExpressionContext containing the - // correct comparator is injected, since the groups must be built using the comparator's - // definition of equality. - boost::optional<GroupsMap> _groups; - - std::vector<std::shared_ptr<Sorter<Value, Value>::Iterator>> _sortedFiles; - bool _spilled; - - // Only used when '_spilled' is false. - GroupsMap::iterator groupsIterator; - - // Only used when '_spilled' is true. - std::unique_ptr<Sorter<Value, Value>::Iterator> _sorterIterator; - - std::pair<Value, Value> _firstPartOfNextGroup; + GetNextResult performBlockingGroupSelf(GetNextResult input); - bool _sbeCompatible; + bool _groupsReady; }; } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_group_base.cpp b/src/mongo/db/pipeline/document_source_group_base.cpp new file mode 100644 index 00000000000..0268cd57127 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_group_base.cpp @@ -0,0 +1,811 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include <memory> + +#include "mongo/db/exec/document_value/document.h" +#include "mongo/db/exec/document_value/value.h" +#include "mongo/db/exec/document_value/value_comparator.h" +#include "mongo/db/pipeline/accumulation_statement.h" +#include "mongo/db/pipeline/accumulator.h" +#include "mongo/db/pipeline/document_source_group.h" +#include "mongo/db/pipeline/document_source_group_base.h" +#include "mongo/db/pipeline/expression.h" +#include "mongo/db/pipeline/expression_context.h" +#include "mongo/db/pipeline/lite_parsed_document_source.h" +#include "mongo/db/stats/resource_consumption_metrics.h" +#include "mongo/util/destructor_guard.h" + +namespace mongo { + +namespace { + +/** + * Generates a new file name on each call using a static, atomic and monotonically increasing + * number. + * + * Each user of the Sorter must implement this function to ensure that all temporary files that the + * Sorter instances produce are uniquely identified using a unique file name extension with separate + * atomic variable. This is necessary because the sorter.cpp code is separately included in multiple + * places, rather than compiled in one place and linked, and so cannot provide a globally unique ID. + */ +std::string nextFileName() { + static AtomicWord<unsigned> documentSourceGroupFileCounter; + return "extsort-doc-group." + std::to_string(documentSourceGroupFileCounter.fetchAndAdd(1)); +} + +} // namespace + +using boost::intrusive_ptr; +using std::pair; +using std::shared_ptr; +using std::vector; + +Value DocumentSourceGroupBase::serialize(boost::optional<ExplainOptions::Verbosity> explain) const { + MutableDocument insides; + + // Add the _id. + if (_idFieldNames.empty()) { + invariant(_idExpressions.size() == 1); + insides["_id"] = _idExpressions[0]->serialize(static_cast<bool>(explain)); + } else { + // Decomposed document case. + invariant(_idExpressions.size() == _idFieldNames.size()); + MutableDocument md; + for (size_t i = 0; i < _idExpressions.size(); i++) { + md[_idFieldNames[i]] = _idExpressions[i]->serialize(static_cast<bool>(explain)); + } + insides["_id"] = md.freezeToValue(); + } + + // Add the remaining fields. + for (auto&& accumulatedField : _accumulatedFields) { + intrusive_ptr<AccumulatorState> accum = accumulatedField.makeAccumulator(); + insides[accumulatedField.fieldName] = + Value(accum->serialize(accumulatedField.expr.initializer, + accumulatedField.expr.argument, + static_cast<bool>(explain))); + } + + if (_doingMerge) { + insides["$doingMerge"] = Value(true); + } + + serializeAdditionalFields(insides, explain); + + MutableDocument out; + out[getSourceName()] = insides.freezeToValue(); + + if (explain && *explain >= ExplainOptions::Verbosity::kExecStats) { + MutableDocument md; + + for (size_t i = 0; i < _accumulatedFields.size(); i++) { + md[_accumulatedFields[i].fieldName] = Value(static_cast<long long>( + _memoryTracker[_accumulatedFields[i].fieldName].maxMemoryBytes())); + } + + out["maxAccumulatorMemoryUsageBytes"] = Value(md.freezeToValue()); + out["totalOutputDataSizeBytes"] = + Value(static_cast<long long>(_stats.totalOutputDataSizeBytes)); + out["usedDisk"] = Value(_stats.spills > 0); + out["spills"] = Value(static_cast<long long>(_stats.spills)); + } + + return out.freezeToValue(); +} + + +bool DocumentSourceGroupBase::shouldSpillWithAttemptToSaveMemory() { + if (!_memoryTracker._allowDiskUse && + (_memoryTracker.currentMemoryBytes() > + static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes))) { + freeMemory(); + } + + if (_memoryTracker.currentMemoryBytes() > + static_cast<long long>(_memoryTracker._maxAllowedMemoryUsageBytes)) { + uassert(ErrorCodes::QueryExceededMemoryLimitNoDiskUseAllowed, + "Exceeded memory limit for $group, but didn't allow external sort." + " Pass allowDiskUse:true to opt in.", + _memoryTracker._allowDiskUse); + _memoryTracker.resetCurrent(); + return true; + } + return false; +} + +void DocumentSourceGroupBase::freeMemory() { + invariant(_groups); + for (auto&& group : *_groups) { + for (size_t i = 0; i < group.second.size(); i++) { + // Subtract the current usage. + _memoryTracker.update(_accumulatedFields[i].fieldName, + -1 * group.second[i]->getMemUsage()); + + group.second[i]->reduceMemoryConsumptionIfAble(); + + // Update the memory usage for this AccumulationStatement. + _memoryTracker.update(_accumulatedFields[i].fieldName, group.second[i]->getMemUsage()); + } + } +} + +DocumentSource::GetNextResult DocumentSourceGroupBase::getNextReadyGroup() { + if (_spilled) { + return getNextSpilled(); + } else { + return getNextStandard(); + } +} + +DocumentSource::GetNextResult DocumentSourceGroupBase::getNextSpilled() { + // We aren't streaming, and we have spilled to disk. + if (!_sorterIterator) + return GetNextResult::makeEOF(); + + Value currentId = _firstPartOfNextGroup.first; + const size_t numAccumulators = _accumulatedFields.size(); + + // Call startNewGroup on every accumulator. + Value expandedId = expandId(currentId); + Document idDoc = + expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); + for (size_t i = 0; i < numAccumulators; ++i) { + Value initializerValue = + _accumulatedFields[i].expr.initializer->evaluate(idDoc, &pExpCtx->variables); + _currentAccumulators[i]->reset(); + _currentAccumulators[i]->startNewGroup(initializerValue); + } + + while (pExpCtx->getValueComparator().evaluate(currentId == _firstPartOfNextGroup.first)) { + // Inside of this loop, _firstPartOfNextGroup is the current data being processed. + // At loop exit, it is the first value to be processed in the next group. + switch (numAccumulators) { // mirrors switch in spill() + case 1: // Single accumulators serialize as a single Value. + _currentAccumulators[0]->process(_firstPartOfNextGroup.second, true); + [[fallthrough]]; + case 0: // No accumulators so no Values. + break; + default: { // Multiple accumulators serialize as an array of Values. + const vector<Value>& accumulatorStates = _firstPartOfNextGroup.second.getArray(); + for (size_t i = 0; i < numAccumulators; i++) { + _currentAccumulators[i]->process(accumulatorStates[i], true); + } + } + } + + if (!_sorterIterator->more()) { + _sorterIterator.reset(); + break; + } + + _firstPartOfNextGroup = _sorterIterator->next(); + } + + return makeDocument(currentId, _currentAccumulators, pExpCtx->needsMerge); +} + +DocumentSource::GetNextResult DocumentSourceGroupBase::getNextStandard() { + // Not spilled, and not streaming. + if (_groupsIterator == _groups->end()) + return GetNextResult::makeEOF(); + + Document out = + makeDocument(_groupsIterator->first, _groupsIterator->second, pExpCtx->needsMerge); + ++_groupsIterator; + return out; +} + +void DocumentSourceGroupBase::doDispose() { + resetReadyGroups(); +} + +intrusive_ptr<DocumentSource> DocumentSourceGroupBase::optimize() { + // Optimizing a 'DocumentSourceGroupBase' might modify its expressions to become incompatible + // with SBE. We temporarily highjack the context's 'sbeCompatible' flag to communicate the + // situation back to the 'DocumentSourceGroupBase'. Notice, that while a particular + // 'DocumentSourceGroupBase' might become incompatible with SBE, other groups in the pipeline + // and the collection access could be still eligible for lowering to SBE, thus we must reset the + // context's 'sbeCompatible' flag back to its original value at the end of the 'optimize()' + // call. + // + // TODO SERVER-XXXXX: replace this hack with a proper per-stage tracking of SBE compatibility. + auto expCtx = _idExpressions[0]->getExpressionContext(); + auto orgSbeCompatible = expCtx->sbeCompatible; + expCtx->sbeCompatible = true; + + // TODO: If all _idExpressions are ExpressionConstants after optimization, then we know there + // will be only one group. We should take advantage of that to avoid going through the hash + // table. + for (size_t i = 0; i < _idExpressions.size(); i++) { + _idExpressions[i] = _idExpressions[i]->optimize(); + } + + for (auto&& accumulatedField : _accumulatedFields) { + accumulatedField.expr.initializer = accumulatedField.expr.initializer->optimize(); + accumulatedField.expr.argument = accumulatedField.expr.argument->optimize(); + } + + _sbeCompatible = _sbeCompatible && expCtx->sbeCompatible; + expCtx->sbeCompatible = orgSbeCompatible; + + return this; +} + +DepsTracker::State DocumentSourceGroupBase::getDependencies(DepsTracker* deps) const { + // add the _id + for (size_t i = 0; i < _idExpressions.size(); i++) { + _idExpressions[i]->addDependencies(deps); + } + + // add the rest + for (auto&& accumulatedField : _accumulatedFields) { + accumulatedField.expr.argument->addDependencies(deps); + // Don't add initializer, because it doesn't refer to docs from the input stream. + } + + return DepsTracker::State::EXHAUSTIVE_ALL; +} + +DocumentSource::GetModPathsReturn DocumentSourceGroupBase::getModifiedPaths() const { + // We preserve none of the fields, but any fields referenced as part of the group key are + // logically just renamed. + StringMap<std::string> renames; + for (std::size_t i = 0; i < _idExpressions.size(); ++i) { + auto idExp = _idExpressions[i]; + auto pathToPutResultOfExpression = + _idFieldNames.empty() ? "_id" : "_id." + _idFieldNames[i]; + auto computedPaths = idExp->getComputedPaths(pathToPutResultOfExpression); + for (auto&& rename : computedPaths.renames) { + renames[rename.first] = rename.second; + } + } + + return {DocumentSource::GetModPathsReturn::Type::kAllExcept, + OrderedPathSet{}, // No fields are preserved. + std::move(renames)}; +} + +StringMap<boost::intrusive_ptr<Expression>> DocumentSourceGroupBase::getIdFields() const { + if (_idFieldNames.empty()) { + invariant(_idExpressions.size() == 1); + return {{"_id", _idExpressions[0]}}; + } else { + invariant(_idFieldNames.size() == _idExpressions.size()); + StringMap<boost::intrusive_ptr<Expression>> result; + for (std::size_t i = 0; i < _idFieldNames.size(); ++i) { + result["_id." + _idFieldNames[i]] = _idExpressions[i]; + } + return result; + } +} + +std::vector<boost::intrusive_ptr<Expression>>& DocumentSourceGroupBase::getMutableIdFields() { + tassert(7020503, "Can't mutate _id fields after initialization", !_executionStarted); + return _idExpressions; +} + +const std::vector<AccumulationStatement>& DocumentSourceGroupBase::getAccumulatedFields() const { + return _accumulatedFields; +} + +std::vector<AccumulationStatement>& DocumentSourceGroupBase::getMutableAccumulatedFields() { + tassert(7020504, "Can't mutate accumulated fields after initialization", !_executionStarted); + return _accumulatedFields; +} + +DocumentSourceGroupBase::DocumentSourceGroupBase(StringData stageName, + const intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes) + : DocumentSource(stageName, expCtx), + _doingMerge(false), + _memoryTracker{expCtx->allowDiskUse && !expCtx->inMongos, + maxMemoryUsageBytes + ? *maxMemoryUsageBytes + : static_cast<size_t>(internalDocumentSourceGroupMaxMemoryBytes.load())}, + _executionStarted(false), + _groups(expCtx->getValueComparator().makeUnorderedValueMap<Accumulators>()), + _spilled(false), + _sbeCompatible(false) {} + +void DocumentSourceGroupBase::addAccumulator(AccumulationStatement accumulationStatement) { + _accumulatedFields.push_back(accumulationStatement); + _memoryTracker.set(accumulationStatement.fieldName, 0); +} + +namespace { + +intrusive_ptr<Expression> parseIdExpression(const intrusive_ptr<ExpressionContext>& expCtx, + BSONElement groupField, + const VariablesParseState& vps) { + if (groupField.type() == Object) { + // {_id: {}} is treated as grouping on a constant, not an expression + if (groupField.Obj().isEmpty()) { + return ExpressionConstant::create(expCtx.get(), Value(groupField)); + } + + const BSONObj idKeyObj = groupField.Obj(); + if (idKeyObj.firstElementFieldName()[0] == '$') { + // grouping on a $op expression + return Expression::parseObject(expCtx.get(), idKeyObj, vps); + } else { + for (auto&& field : idKeyObj) { + uassert(17390, + "$group does not support inclusion-style expressions", + !field.isNumber() && field.type() != Bool); + } + return ExpressionObject::parse(expCtx.get(), idKeyObj, vps); + } + } else { + return Expression::parseOperand(expCtx.get(), groupField, vps); + } +} + +} // namespace + +void DocumentSourceGroupBase::setIdExpression(const boost::intrusive_ptr<Expression> idExpression) { + if (auto object = dynamic_cast<ExpressionObject*>(idExpression.get())) { + auto& childExpressions = object->getChildExpressions(); + invariant(!childExpressions.empty()); // We expect to have converted an empty object into a + // constant expression. + + // grouping on an "artificial" object. Rather than create the object for each input + // in initialize(), instead group on the output of the raw expressions. The artificial + // object will be created at the end in makeDocument() while outputting results. + for (auto&& childExpPair : childExpressions) { + _idFieldNames.push_back(childExpPair.first); + _idExpressions.push_back(childExpPair.second); + } + } else { + _idExpressions.push_back(idExpression); + } +} + +boost::intrusive_ptr<Expression> DocumentSourceGroupBase::getIdExpression() const { + // _idFieldNames is empty and _idExpressions has one element when the _id expression is not an + // object expression. + if (_idFieldNames.empty() && _idExpressions.size() == 1) { + return _idExpressions[0]; + } + + tassert(6586300, + "Field and its expression must be always paired in ExpressionObject", + _idFieldNames.size() > 0 && _idFieldNames.size() == _idExpressions.size()); + + // Each expression in '_idExpressions' may have been optimized and so, compose the object _id + // expression out of the optimized expressions. + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fieldsAndExprs; + for (size_t i = 0; i < _idExpressions.size(); ++i) { + fieldsAndExprs.emplace_back(_idFieldNames[i], _idExpressions[i]); + } + + return ExpressionObject::create(_idExpressions[0]->getExpressionContext(), + std::move(fieldsAndExprs)); +} + +void DocumentSourceGroupBase::initializeFromBson(BSONElement elem) { + uassert(15947, "a group's fields must be specified in an object", elem.type() == Object); + + BSONObj groupObj(elem.Obj()); + BSONObjIterator groupIterator(groupObj); + VariablesParseState vps = pExpCtx->variablesParseState; + pExpCtx->sbeGroupCompatible = true; + while (groupIterator.more()) { + BSONElement groupField(groupIterator.next()); + StringData pFieldName = groupField.fieldNameStringData(); + if (pFieldName == "_id") { + uassert(15948, "a group's _id may only be specified once", _idExpressions.empty()); + setIdExpression(parseIdExpression(pExpCtx, groupField, vps)); + invariant(!_idExpressions.empty()); + } else if (pFieldName == "$doingMerge") { + massert(17030, "$doingMerge should be true if present", groupField.Bool()); + + setDoingMerge(true); + } else if (isSpecFieldReserved(pFieldName)) { + // No-op: field is used by the derived class. + } else { + // Any other field will be treated as an accumulator specification. + addAccumulator( + AccumulationStatement::parseAccumulationStatement(pExpCtx.get(), groupField, vps)); + } + } + _sbeCompatible = pExpCtx->sbeGroupCompatible && pExpCtx->sbeCompatible; + + uassert(15955, "a group specification must include an _id", !_idExpressions.empty()); +} + +namespace { + +using GroupsMap = DocumentSourceGroupBase::GroupsMap; + +class SorterComparator { +public: + typedef pair<Value, Value> Data; + + SorterComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} + + int operator()(const Data& lhs, const Data& rhs) const { + return _valueComparator.compare(lhs.first, rhs.first); + } + +private: + ValueComparator _valueComparator; +}; + +class SpillSTLComparator { +public: + SpillSTLComparator(ValueComparator valueComparator) : _valueComparator(valueComparator) {} + + bool operator()(const GroupsMap::value_type* lhs, const GroupsMap::value_type* rhs) const { + return _valueComparator.evaluate(lhs->first < rhs->first); + } + +private: + ValueComparator _valueComparator; +}; +} // namespace + +void DocumentSourceGroupBase::processDocument(const Value& id, const Document& root) { + const size_t numAccumulators = _accumulatedFields.size(); + + // Look for the _id value in the map. If it's not there, add a new entry with a blank + // accumulator. This is done in a somewhat odd way in order to avoid hashing 'id' and + // looking it up in '_groups' multiple times. + const size_t oldSize = _groups->size(); + vector<intrusive_ptr<AccumulatorState>>& group = (*_groups)[id]; + const bool inserted = _groups->size() != oldSize; + + if (inserted) { + _memoryTracker.set(_memoryTracker.currentMemoryBytes() + id.getApproximateSize()); + + // Initialize and add the accumulators + Value expandedId = expandId(id); + Document idDoc = + expandedId.getType() == BSONType::Object ? expandedId.getDocument() : Document(); + group.reserve(numAccumulators); + for (auto&& accumulatedField : _accumulatedFields) { + auto accum = accumulatedField.makeAccumulator(); + Value initializerValue = + accumulatedField.expr.initializer->evaluate(idDoc, &pExpCtx->variables); + accum->startNewGroup(initializerValue); + group.push_back(accum); + } + } + + /* tickle all the accumulators for the group we found */ + dassert(numAccumulators == group.size()); + + for (size_t i = 0; i < numAccumulators; i++) { + // Only process the input and update the memory footprint if the current accumulator + // needs more input. + if (group[i]->needsInput()) { + const auto prevMemUsage = inserted ? 0 : group[i]->getMemUsage(); + group[i]->process( + _accumulatedFields[i].expr.argument->evaluate(root, &pExpCtx->variables), + _doingMerge); + _memoryTracker.update(_accumulatedFields[i].fieldName, + group[i]->getMemUsage() - prevMemUsage); + } + } + + if (kDebugBuild && !storageGlobalParams.readOnly) { + // In debug mode, spill every time we have a duplicate id to stress merge logic. + if (!inserted && // is a dup + !pExpCtx->inMongos && // can't spill to disk in mongos + !_memoryTracker._allowDiskUse && // don't change behavior when testing external sort + _sortedFiles.size() < 20) { // don't open too many FDs + spill(); + } + } +} + +void DocumentSourceGroupBase::readyGroups() { + _spilled = !_sortedFiles.empty(); + if (_spilled) { + if (!_groups->empty()) { + spill(); + } + + _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); + + _sorterIterator.reset(Sorter<Value, Value>::Iterator::merge( + _sortedFiles, SortOptions(), SorterComparator(pExpCtx->getValueComparator()))); + + // prepare current to accumulate data + _currentAccumulators.reserve(_accumulatedFields.size()); + for (auto&& accumulatedField : _accumulatedFields) { + _currentAccumulators.push_back(accumulatedField.makeAccumulator()); + } + + verify(_sorterIterator->more()); // we put data in, we should get something out. + _firstPartOfNextGroup = _sorterIterator->next(); + } else { + // start the group iterator + _groupsIterator = _groups->begin(); + } +} + +void DocumentSourceGroupBase::resetReadyGroups() { + // Free our resources. + _groups = pExpCtx->getValueComparator().makeUnorderedValueMap<Accumulators>(); + _memoryTracker.resetCurrent(); + _sorterIterator.reset(); + _sortedFiles.clear(); + + // Make us look done. + _groupsIterator = _groups->end(); +} + +void DocumentSourceGroupBase::spill() { + _stats.spills++; + + vector<const GroupsMap::value_type*> ptrs; // using pointers to speed sorting + ptrs.reserve(_groups->size()); + for (GroupsMap::const_iterator it = _groups->begin(), end = _groups->end(); it != end; ++it) { + ptrs.push_back(&*it); + } + + stable_sort(ptrs.begin(), ptrs.end(), SpillSTLComparator(pExpCtx->getValueComparator())); + + // Initialize '_file' in a lazy manner only when it is needed. + if (!_file) { + _file = + std::make_shared<Sorter<Value, Value>::File>(pExpCtx->tempDir + "/" + nextFileName()); + } + SortedFileWriter<Value, Value> writer(SortOptions().TempDir(pExpCtx->tempDir), _file); + switch (_accumulatedFields.size()) { // same as ptrs[i]->second.size() for all i. + case 0: // no values, essentially a distinct + for (size_t i = 0; i < ptrs.size(); i++) { + writer.addAlreadySorted(ptrs[i]->first, Value()); + } + break; + + case 1: // just one value, use optimized serialization as single Value + for (size_t i = 0; i < ptrs.size(); i++) { + writer.addAlreadySorted(ptrs[i]->first, + ptrs[i]->second[0]->getValue(/*toBeMerged=*/true)); + } + break; + + default: // multiple values, serialize as array-typed Value + for (size_t i = 0; i < ptrs.size(); i++) { + vector<Value> accums; + for (size_t j = 0; j < ptrs[i]->second.size(); j++) { + accums.push_back(ptrs[i]->second[j]->getValue(/*toBeMerged=*/true)); + } + writer.addAlreadySorted(ptrs[i]->first, Value(std::move(accums))); + } + break; + } + + auto& metricsCollector = ResourceConsumption::MetricsCollector::get(pExpCtx->opCtx); + metricsCollector.incrementKeysSorted(ptrs.size()); + metricsCollector.incrementSorterSpills(1); + + _groups->clear(); + // Zero out the current per-accumulation statement memory consumption, as the memory has been + // freed by spilling. + for (const auto& accum : _accumulatedFields) { + _memoryTracker.set(accum.fieldName, 0); + } + + _sortedFiles.emplace_back(writer.done()); +} + +Value DocumentSourceGroupBase::computeId(const Document& root) { + // If only one expression, return result directly + if (_idExpressions.size() == 1) { + Value retValue = _idExpressions[0]->evaluate(root, &pExpCtx->variables); + return retValue.missing() ? Value(BSONNULL) : std::move(retValue); + } + + // Multiple expressions get results wrapped in a vector + vector<Value> vals; + vals.reserve(_idExpressions.size()); + for (size_t i = 0; i < _idExpressions.size(); i++) { + vals.push_back(_idExpressions[i]->evaluate(root, &pExpCtx->variables)); + } + return Value(std::move(vals)); +} + +Value DocumentSourceGroupBase::expandId(const Value& val) { + // _id doesn't get wrapped in a document + if (_idFieldNames.empty()) + return val; + + // _id is a single-field document containing val + if (_idFieldNames.size() == 1) + return Value(DOC(_idFieldNames[0] << val)); + + // _id is a multi-field document containing the elements of val + const vector<Value>& vals = val.getArray(); + invariant(_idFieldNames.size() == vals.size()); + MutableDocument md(vals.size()); + for (size_t i = 0; i < vals.size(); i++) { + md[_idFieldNames[i]] = vals[i]; + } + return md.freezeToValue(); +} + +Document DocumentSourceGroupBase::makeDocument(const Value& id, + const Accumulators& accums, + bool mergeableOutput) { + const size_t n = _accumulatedFields.size(); + MutableDocument out(1 + n); + + /* add the _id field */ + out.addField("_id", expandId(id)); + + /* add the rest of the fields */ + for (size_t i = 0; i < n; ++i) { + Value val = accums[i]->getValue(mergeableOutput); + if (val.missing()) { + // we return null in this case so return objects are predictable + out.addField(_accumulatedFields[i].fieldName, Value(BSONNULL)); + } else { + out.addField(_accumulatedFields[i].fieldName, std::move(val)); + } + } + + _stats.totalOutputDataSizeBytes += out.getApproximateSize(); + return out.freeze(); +} + +bool DocumentSourceGroupBase::pathIncludedInGroupKeys(const std::string& dottedPath) const { + return std::any_of( + _idExpressions.begin(), _idExpressions.end(), [&dottedPath](const auto& exp) { + if (auto fieldExp = dynamic_cast<ExpressionFieldPath*>(exp.get())) { + if (fieldExp->representsPath(dottedPath)) { + return true; + } + } + return false; + }); +} + +bool DocumentSourceGroupBase::canRunInParallelBeforeWriteStage( + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const { + if (_doingMerge) { + return true; // This is fine. + } + + // Certain $group stages are allowed to execute on each exchange consumer. In order to + // guarantee each consumer will only group together data from its own shard, the $group must + // group on a superset of the shard key. + for (auto&& currentPathOfShardKey : nameOfShardKeyFieldsUponEntryToStage) { + if (!pathIncludedInGroupKeys(currentPathOfShardKey)) { + // This requires an exact path match, but as a future optimization certain path + // prefixes should be okay. For example, if the shard key path is "a.b", and we're + // grouping by "a", then each group of "a" is strictly more specific than "a.b", so + // we can deduce that grouping by "a" will not need to group together documents + // across different values of the shard key field "a.b", and thus as long as any + // other shard key fields are similarly preserved will not need to consume a merged + // stream to perform the group. + return false; + } + } + return true; +} + +std::unique_ptr<GroupFromFirstDocumentTransformation> +DocumentSourceGroupBase::rewriteGroupAsTransformOnFirstDocument() const { + if (_idExpressions.size() != 1) { + // This transformation is only intended for $group stages that group on a single field. + return nullptr; + } + + auto fieldPathExpr = dynamic_cast<ExpressionFieldPath*>(_idExpressions.front().get()); + if (!fieldPathExpr || fieldPathExpr->isVariableReference()) { + return nullptr; + } + + const auto fieldPath = fieldPathExpr->getFieldPath(); + if (fieldPath.getPathLength() == 1) { + // The path is $$CURRENT or $$ROOT. This isn't really a sensible value to group by (since + // each document has a unique _id, it will just return the entire collection). We only + // apply the rewrite when grouping by a single field, so we cannot apply it in this case, + // where we are grouping by the entire document. + tassert(5943200, + "Optimization attempted on group by always-dissimilar system variable", + fieldPath.getFieldName(0) == "CURRENT" || fieldPath.getFieldName(0) == "ROOT"); + return nullptr; + } + + const auto groupId = fieldPath.tail().fullPath(); + + // We can't do this transformation if there are any non-$first accumulators. + for (auto&& accumulator : _accumulatedFields) { + if (AccumulatorDocumentsNeeded::kFirstDocument != + accumulator.makeAccumulator()->documentsNeeded()) { + return nullptr; + } + } + + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> fields; + + boost::intrusive_ptr<Expression> idField; + // The _id field can be specified either as a fieldpath (ex. _id: "$a") or as a singleton + // object (ex. _id: {v: "$a"}). + if (_idFieldNames.empty()) { + idField = ExpressionFieldPath::deprecatedCreate(pExpCtx.get(), groupId); + } else { + invariant(_idFieldNames.size() == 1); + idField = ExpressionObject::create(pExpCtx.get(), + {{_idFieldNames.front(), _idExpressions.front()}}); + } + fields.push_back(std::make_pair("_id", idField)); + + for (auto&& accumulator : _accumulatedFields) { + fields.push_back(std::make_pair(accumulator.fieldName, accumulator.expr.argument)); + + // Since we don't attempt this transformation for non-$first accumulators, + // the initializer should always be trivial. + } + + return GroupFromFirstDocumentTransformation::create( + pExpCtx, groupId, getSourceName(), std::move(fields)); +} + +size_t DocumentSourceGroupBase::getMaxMemoryUsageBytes() const { + return _memoryTracker._maxAllowedMemoryUsageBytes; +} + +boost::optional<DocumentSource::DistributedPlanLogic> +DocumentSourceGroupBase::distributedPlanLogic() { + VariablesParseState vps = pExpCtx->variablesParseState; + /* the merger will use the same grouping key */ + auto mergerGroupByExpression = ExpressionFieldPath::parse(pExpCtx.get(), "$$ROOT._id", vps); + + std::vector<AccumulationStatement> mergerAccumulators; + mergerAccumulators.reserve(_accumulatedFields.size()); + for (auto&& accumulatedField : _accumulatedFields) { + // The merger's output field names will be the same, as will the accumulator factories. + // However, for some accumulators, the expression to be accumulated will be different. The + // original accumulator may be collecting an expression based on a field expression or + // constant. Here, we accumulate the output of the same name from the prior group. + auto copiedAccumulatedField = accumulatedField; + copiedAccumulatedField.expr.argument = ExpressionFieldPath::parse( + pExpCtx.get(), "$$ROOT." + copiedAccumulatedField.fieldName, vps); + mergerAccumulators.emplace_back(std::move(copiedAccumulatedField)); + } + + // When merging, we always use generic hash based algorithm. + boost::intrusive_ptr<DocumentSourceGroup> mergingGroup = DocumentSourceGroup::create( + pExpCtx, std::move(mergerGroupByExpression), std::move(mergerAccumulators)); + mergingGroup->setDoingMerge(true); + // {shardsStage, mergingStage, sortPattern} + return DistributedPlanLogic{this, mergingGroup, boost::none}; +} + +} // namespace mongo + +#include "mongo/db/sorter/sorter.cpp" +// Explicit instantiation unneeded since we aren't exposing Sorter outside of this file. diff --git a/src/mongo/db/pipeline/document_source_group_base.h b/src/mongo/db/pipeline/document_source_group_base.h new file mode 100644 index 00000000000..76d099fef78 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_group_base.h @@ -0,0 +1,266 @@ +/** + * Copyright (C) 2018-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <memory> +#include <utility> + +#include "mongo/db/pipeline/accumulation_statement.h" +#include "mongo/db/pipeline/accumulator.h" +#include "mongo/db/pipeline/document_source.h" +#include "mongo/db/pipeline/group_from_first_document_transformation.h" +#include "mongo/db/pipeline/memory_usage_tracker.h" +#include "mongo/db/sorter/sorter.h" + +namespace mongo { + +/** + * This class represents a $group stage generically - could be a streaming or hash based group. + * + * It contains some common execution code between the two algorithms, such as: + * - Handling spilling to disk. + * - Computing the group key + * - Accumulating values and populating output documents. + */ +class DocumentSourceGroupBase : public DocumentSource { +public: + using Accumulators = std::vector<boost::intrusive_ptr<AccumulatorState>>; + using GroupsMap = ValueUnorderedMap<Accumulators>; + + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; + boost::intrusive_ptr<DocumentSource> optimize() final; + DepsTracker::State getDependencies(DepsTracker* deps) const final; + GetModPathsReturn getModifiedPaths() const final; + StringMap<boost::intrusive_ptr<Expression>> getIdFields() const; + + boost::optional<DistributedPlanLogic> distributedPlanLogic() final; + + /** + * Can be used to change or swap out individual _id fields, but should not be used + * once execution has begun. + */ + std::vector<boost::intrusive_ptr<Expression>>& getMutableIdFields(); + const std::vector<AccumulationStatement>& getAccumulatedFields() const; + + /** + * Can be used to change or swap out individual accumulated fields, but should not be used + * once execution has begun. + */ + std::vector<AccumulationStatement>& getMutableAccumulatedFields(); + + StageConstraints constraints(Pipeline::SplitState pipeState) const final { + StageConstraints constraints(StreamType::kBlocking, + PositionRequirement::kNone, + HostTypeRequirement::kNone, + DiskUseRequirement::kWritesTmpData, + FacetRequirement::kAllowed, + TransactionRequirement::kAllowed, + LookupRequirement::kAllowed, + UnionRequirement::kAllowed); + constraints.canSwapWithMatch = true; + return constraints; + } + + /** + * Add an accumulator, which will become a field in each Document that results from grouping. + */ + void addAccumulator(AccumulationStatement accumulationStatement); + + /** + * Sets the expression to use to determine the group id of each document. + */ + void setIdExpression(boost::intrusive_ptr<Expression> idExpression); + + /** + * Returns the expression to use to determine the group id of each document. + */ + boost::intrusive_ptr<Expression> getIdExpression() const; + + /** + * Returns true if this $group stage represents a 'global' $group which is merging together + * results from earlier partial groups. + */ + bool doingMerge() const { + return _doingMerge; + } + + /** + * Tell this source if it is doing a merge from shards. Defaults to false. + */ + void setDoingMerge(bool doingMerge) { + _doingMerge = doingMerge; + } + + /** + * Returns true if this $group stage used disk during execution and false otherwise. + */ + bool usedDisk() final { + return _stats.spills > 0; + } + + const SpecificStats* getSpecificStats() const final { + return &_stats; + } + + bool canRunInParallelBeforeWriteStage( + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const final; + + /** + * When possible, creates a document transformer that transforms the first document in a group + * into one of the output documents of the $group stage. This is possible when we are grouping + * on a single field and all accumulators are $first (or there are no accumluators). + * + * It is sometimes possible to use a DISTINCT_SCAN to scan the first document of each group, + * in which case this transformation can replace the actual $group stage in the pipeline + * (SERVER-9507). + */ + std::unique_ptr<GroupFromFirstDocumentTransformation> rewriteGroupAsTransformOnFirstDocument() + const; + + /** + * Returns maximum allowed memory footprint. + */ + size_t getMaxMemoryUsageBytes() const; + + // True if this $group can be pushed down to SBE. + bool sbeCompatible() const { + return _sbeCompatible; + } + +protected: + DocumentSourceGroupBase(StringData stageName, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes = boost::none); + + void initializeFromBson(BSONElement elem); + virtual bool isSpecFieldReserved(StringData fieldName) = 0; + + void doDispose() final; + + /** + * Cleans up any pending memory usage. Throws error, if memory usage is above + * 'maxMemoryUsageBytes' and cannot spill to disk. + * + * Returns true, if the caller should spill to disk, false otherwise. + */ + bool shouldSpillWithAttemptToSaveMemory(); + + /** + * Spill groups map to disk and returns an iterator to the file. Note: Since a sorted $group + * does not exhaust the previous stage before returning, and thus does not maintain as large a + * store of documents at any one time, only an unsorted group can spill to disk. + */ + void spill(); + + /** + * Computes the internal representation of the group key. + */ + Value computeId(const Document& root); + + void processDocument(const Value& id, const Document& root); + + void readyGroups(); + void resetReadyGroups(); + + GetNextResult getNextReadyGroup(); + + void setExecutionStarted() { + _executionStarted = true; + } + + virtual void serializeAdditionalFields( + MutableDocument& out, boost::optional<ExplainOptions::Verbosity> explain) const {}; + + // If the expression for the '_id' field represents a non-empty object, we track its fields' + // names in '_idFieldNames'. + std::vector<std::string> _idFieldNames; + // Expressions for the individual fields when '_id' produces a document in the order of + // '_idFieldNames' or the whole expression otherwise. + std::vector<boost::intrusive_ptr<Expression>> _idExpressions; + +private: + GetNextResult getNextSpilled(); + GetNextResult getNextStandard(); + + /** + * If we ran out of memory, finish all the pending operations so that some memory + * can be freed. + */ + void freeMemory(); + + Document makeDocument(const Value& id, const Accumulators& accums, bool mergeableOutput); + + /** + * Converts the internal representation of the group key to the _id shape specified by the + * user. + */ + Value expandId(const Value& val); + + /** + * Returns true if 'dottedPath' is one of the group keys present in '_idExpressions'. + */ + bool pathIncludedInGroupKeys(const std::string& dottedPath) const; + + std::vector<AccumulationStatement> _accumulatedFields; + + bool _doingMerge; + + MemoryUsageTracker _memoryTracker; + + GroupStats _stats; + + /** + * This flag should be set during first execution of getNext() to assert that non-const methods + * that expose internal structures are not called during runtime. + */ + bool _executionStarted; + + // We use boost::optional to defer initialization until the ExpressionContext containing the + // correct comparator is injected, since the groups must be built using the comparator's + // definition of equality. + boost::optional<GroupsMap> _groups; + + std::shared_ptr<Sorter<Value, Value>::File> _file; + std::vector<std::shared_ptr<Sorter<Value, Value>::Iterator>> _sortedFiles; + bool _spilled; + + // Only used when '_spilled' is false. + GroupsMap::iterator _groupsIterator; + + // Only used when '_spilled' is true. + std::unique_ptr<Sorter<Value, Value>::Iterator> _sorterIterator; + + std::pair<Value, Value> _firstPartOfNextGroup; + Accumulators _currentAccumulators; + + bool _sbeCompatible; +}; + +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_group_test.cpp b/src/mongo/db/pipeline/document_source_group_test.cpp index d3ed11f2031..9a621cffdcd 100644 --- a/src/mongo/db/pipeline/document_source_group_test.cpp +++ b/src/mongo/db/pipeline/document_source_group_test.cpp @@ -48,6 +48,7 @@ #include "mongo/db/pipeline/dependencies.h" #include "mongo/db/pipeline/document_source_group.h" #include "mongo/db/pipeline/document_source_mock.h" +#include "mongo/db/pipeline/document_source_streaming_group.h" #include "mongo/db/pipeline/expression.h" #include "mongo/db/pipeline/expression_context_for_test.h" #include "mongo/db/query/query_test_service_context.h" @@ -252,32 +253,65 @@ BSONObj toBson(const intrusive_ptr<DocumentSource>& source) { return arr[0].getDocument().toBson(); } +enum class GroupStageType { Default, Streaming }; + class Base : public ServiceContextTest { public: - Base() + Base(GroupStageType groupStageType = GroupStageType::Default) : _opCtx(makeOperationContext()), _ctx(new ExpressionContextForTest(_opCtx.get(), AggregateCommandRequest(NamespaceString(ns), {}))), - _tempDir("DocumentSourceGroupTest") {} + _tempDir("DocumentSourceGroupTest"), + _groupStageType(groupStageType) {} protected: + StringData getStageName() const { + switch (_groupStageType) { + case GroupStageType::Default: + return DocumentSourceGroup::kStageName; + case GroupStageType::Streaming: + return DocumentSourceStreamingGroup::kStageName; + default: + MONGO_UNREACHABLE; + } + } + + virtual boost::optional<size_t> getMaxMemoryUsageBytes() { + return boost::none; + } + + intrusive_ptr<DocumentSource> createFromBson( + BSONElement specElement, intrusive_ptr<ExpressionContext> expressionContext) { + switch (_groupStageType) { + case GroupStageType::Default: + return DocumentSourceGroup::createFromBsonWithMaxMemoryUsage( + std::move(specElement), expressionContext, getMaxMemoryUsageBytes()); + case GroupStageType::Streaming: + return DocumentSourceStreamingGroup::createFromBsonWithMaxMemoryUsage( + std::move(specElement), expressionContext, getMaxMemoryUsageBytes()); + default: + MONGO_UNREACHABLE; + } + } + void createGroup(const BSONObj& spec, bool inShard = false, bool inMongos = false) { - BSONObj namedSpec = BSON("$group" << spec); + BSONObj namedSpec = BSON(getStageName() << spec); BSONElement specElement = namedSpec.firstElement(); intrusive_ptr<ExpressionContextForTest> expressionContext = new ExpressionContextForTest( _opCtx.get(), AggregateCommandRequest(NamespaceString(ns), {})); + expressionContext->allowDiskUse = true; // For $group, 'inShard' implies 'fromMongos' and 'needsMerge'. expressionContext->fromMongos = expressionContext->needsMerge = inShard; expressionContext->inMongos = inMongos; // Won't spill to disk properly if it needs to. expressionContext->tempDir = _tempDir.path(); - _group = DocumentSourceGroup::createFromBson(specElement, expressionContext); + _group = createFromBson(specElement, expressionContext); assertRoundTrips(_group, expressionContext); } - DocumentSourceGroup* group() { - return static_cast<DocumentSourceGroup*>(_group.get()); + DocumentSourceGroupBase* group() { + return static_cast<DocumentSourceGroupBase*>(_group.get()); } /** Assert that iterator state accessors consistently report the source is exhausted. */ void assertEOF(const intrusive_ptr<DocumentSource>& source) const { @@ -299,8 +333,7 @@ private: // $const operators may be introduced in the first serialization. BSONObj spec = toBson(group); BSONElement specElement = spec.firstElement(); - intrusive_ptr<DocumentSource> generated = - DocumentSourceGroup::createFromBson(specElement, expCtx); + intrusive_ptr<DocumentSource> generated = createFromBson(specElement, expCtx); ASSERT_BSONOBJ_EQ(spec, toBson(generated)); } std::unique_ptr<QueryTestServiceContext> _queryServiceContext; @@ -308,6 +341,7 @@ private: intrusive_ptr<ExpressionContextForTest> _ctx; intrusive_ptr<DocumentSource> _group; TempDir _tempDir; + GroupStageType _groupStageType; }; class ParseErrorBase : public Base { @@ -355,10 +389,9 @@ class IdConstantBase : public ExpressionBase { class NonObject : public Base { public: void _doTest() final { - BSONObj spec = BSON("$group" - << "foo"); + BSONObj spec = BSON(getStageName() << "foo"); BSONElement specElement = spec.firstElement(); - ASSERT_THROWS(DocumentSourceGroup::createFromBson(specElement, ctx()), AssertionException); + ASSERT_THROWS(createFromBson(specElement, ctx()), AssertionException); } }; @@ -557,8 +590,10 @@ typedef map<Value, Document, ValueCmp> IdMap; class CheckResultsBase : public Base { public: + CheckResultsBase(GroupStageType groupStageType = GroupStageType::Default) + : Base(groupStageType) {} virtual ~CheckResultsBase() {} - void _doTest() { + void _doTest() override { runSharded(false); runSharded(true); } @@ -571,7 +606,7 @@ public: if (sharded) { sink = createMerger(); // Serialize and re-parse the shard stage. - createGroup(toBson(group())["$group"].Obj(), true); + createGroup(toBson(group())[group()->getSourceName()].Obj(), true); group()->setSource(source.get()); sink->setSource(group()); } @@ -871,6 +906,277 @@ public: } }; +class StreamingSimple final : public CheckResultsBase { +public: + StreamingSimple() : CheckResultsBase(GroupStageType::Streaming) {} + +private: + deque<DocumentSource::GetNextResult> inputData() final { + return {Document(BSON("a" << 1 << "b" << 1)), + Document(BSON("a" << 1 << "b" << 2)), + Document(BSON("a" << 2 << "b" << 3)), + Document(BSON("a" << 2 << "b" << 1))}; + } + BSONObj groupSpec() final { + return BSON("_id" + << "$a" + << "sum" + << BSON("$sum" + << "$b") + << "$monotonicIdFields" << BSON_ARRAY("_id")); + } + string expectedResultSetString() final { + return "[{_id:1,sum:3},{_id:2,sum:4}]"; + } +}; + +constexpr size_t kBigStringSize = 1024; +const std::string kBigString(kBigStringSize, 'a'); + +class CheckResultsAndSpills : public CheckResultsBase { +public: + CheckResultsAndSpills(GroupStageType groupStageType, uint64_t expectedSpills) + : CheckResultsBase(groupStageType), _expectedSpills(expectedSpills) {} + + void _doTest() final { + for (int sharded = 0; sharded < 2; ++sharded) { + runSharded(sharded); + const auto* groupStats = static_cast<const GroupStats*>(group()->getSpecificStats()); + ASSERT_EQ(groupStats->spills, _expectedSpills); + } + } + +private: + uint64_t _expectedSpills; +}; + +template <GroupStageType groupStageType, uint64_t expectedSpills> +class StreamingSpillTest : public CheckResultsAndSpills { +public: + StreamingSpillTest() : CheckResultsAndSpills(groupStageType, expectedSpills) {} + +private: + static constexpr int kCount = 11; + + deque<DocumentSource::GetNextResult> inputData() final { + deque<DocumentSource::GetNextResult> queue; + for (int i = 0; i < kCount; ++i) { + queue.emplace_back(Document(BSON("a" << i << "b" << kBigString))); + } + return queue; + } + + BSONObj groupSpec() final { + if constexpr (groupStageType == GroupStageType::Streaming) { + return fromjson("{_id: '$a', big_array: {$push: '$b'}, $monotonicIdFields: ['_id']}"); + } else { + return fromjson("{_id: '$a', big_array: {$push: '$b'}}"); + } + } + + boost::optional<size_t> getMaxMemoryUsageBytes() final { + return 10 * kBigStringSize; + } + + BSONObj expectedResultSet() final { + BSONArrayBuilder result; + for (int i = 0; i < kCount; ++i) { + result << BSON("_id" << i << "big_array" << BSON_ARRAY(kBigString)); + } + return result.arr(); + } +}; + +class WithoutStreamingSpills final + : public StreamingSpillTest<GroupStageType::Default, 2 /*expectedSpills*/> {}; +class StreamingDoesNotSpill final + : public StreamingSpillTest<GroupStageType::Streaming, 0 /*expectedSpills*/> {}; + +class StreamingCanSpill final : public CheckResultsAndSpills { +public: + StreamingCanSpill() : CheckResultsAndSpills(GroupStageType::Streaming, 2 /*expectedSpills*/) {} + +private: + static constexpr int kCount = 11; + + deque<DocumentSource::GetNextResult> inputData() final { + deque<DocumentSource::GetNextResult> queue; + for (int i = 0; i < kCount; ++i) { + queue.emplace_back(Document(BSON("x" << 0 << "y" << i << "b" << kBigString))); + } + return queue; + } + + BSONObj groupSpec() final { + auto id = BSON("x" + << "$x" + << "y" + << "$y"); + return BSON("_id" << id << "big_array" + << BSON("$push" + << "$b") + << "$monotonicIdFields" << BSON_ARRAY("x")); + } + + boost::optional<size_t> getMaxMemoryUsageBytes() final { + return 10 * kBigStringSize; + } + + BSONObj expectedResultSet() final { + BSONArrayBuilder result; + for (int i = 0; i < kCount; ++i) { + auto id = BSON("x" << 0 << "y" << i); + result << BSON("_id" << id << "big_array" << BSON_ARRAY(kBigString)); + } + return result.arr(); + } +}; + +class StreamingAlternatingSpillAndNoSpillBatches : public CheckResultsAndSpills { +public: + StreamingAlternatingSpillAndNoSpillBatches() + : CheckResultsAndSpills(GroupStageType::Streaming, 3 /*expectedSpills*/) {} + +private: + static constexpr int kCount = 12; + + deque<DocumentSource::GetNextResult> inputData() final { + deque<DocumentSource::GetNextResult> queue; + for (int i = 0; i < kCount; ++i) { + // For groups with i % 3 == 0 and i % 3 == 1 there should be no spilling, but groups + // with i % 3 == 2 should spill. + for (int j = 0; j < (i % 3) + 1; ++j) { + queue.emplace_back(Document(BSON("a" << i << "b" << kBigString))); + } + } + return queue; + } + + BSONObj groupSpec() final { + return BSON("_id" + << "$a" + << "big_array" + << BSON("$push" + << "$b") + << "$monotonicIdFields" << BSON_ARRAY("_id")); + } + + boost::optional<size_t> getMaxMemoryUsageBytes() final { + return (25 * kBigStringSize) / 10; + } + + BSONObj expectedResultSet() final { + BSONArrayBuilder result; + for (int i = 0; i < kCount; ++i) { + BSONArrayBuilder bigArrayBuilder; + for (int j = 0; j < (i % 3) + 1; ++j) { + bigArrayBuilder << kBigString; + } + result << BSON("_id" << i << "big_array" << bigArrayBuilder.arr()); + } + return result.arr(); + } +}; + +class StreamingComplex final : public CheckResultsBase { +public: + StreamingComplex() : CheckResultsBase(GroupStageType::Streaming) {} + +private: + static constexpr int kCount = 3; + + deque<DocumentSource::GetNextResult> inputData() final { + deque<DocumentSource::GetNextResult> queue; + for (int i = 0; i < kCount; ++i) { + for (int j = 0; j < kCount; ++j) { + for (int k = 0; k < kCount; ++k) { + queue.emplace_back(Document(BSON("x" << i << "y" << j << "z" << k))); + } + } + } + return queue; + } + + BSONObj groupSpec() final { + BSONObj id = BSON("x" + << "$x" + << "y" + << "$y"); + return BSON("_id" << id << "sum" + << BSON("$sum" + << "$z") + << "$monotonicIdFields" << BSON_ARRAY("x")); + } + + boost::optional<size_t> getMaxMemoryUsageBytes() final { + return 10 * kBigStringSize; + } + + BSONObj expectedResultSet() final { + BSONArrayBuilder result; + for (int i = 0; i < kCount; ++i) { + for (int j = 0; j < kCount; ++j) { + result << BSON("_id" << BSON("x" << i << "y" << j) << "sum" + << (kCount * (kCount - 1)) / 2); + } + } + return result.arr(); + } +}; + +class StreamingMultipleMonotonicFields final : public CheckResultsBase { +public: + StreamingMultipleMonotonicFields() : CheckResultsBase(GroupStageType::Streaming) {} + +private: + static constexpr int kCount = 6; + deque<DocumentSource::GetNextResult> inputData() final { + deque<DocumentSource::GetNextResult> queue; + generateInputOutput([&queue](int x, int y) { + for (int i = 0; i < kCount; ++i) { + queue.emplace_back(Document(BSON("x" << x << "y" << y << "z" << i))); + } + }); + return queue; + } + + BSONObj groupSpec() final { + return fromjson( + "{_id: {x: '$x', y: '$y'}, sum: {$sum: '$z'}, $monotonicIdFields: ['x', 'y']}"); + } + + boost::optional<size_t> getMaxMemoryUsageBytes() final { + return 10 * kBigStringSize; + } + + BSONObj expectedResultSet() final { + BSONArrayBuilder result; + const int sum = (kCount * (kCount - 1)) / 2; + generateInputOutput([&](int x, int y) { + result << BSON("_id" << BSON("x" << x << "y" << y) << "sum" << sum); + }); + return result.arr(); + } + + template <typename Callback> + void generateInputOutput(const Callback& callback) { + int x = 0; + int y = 0; + for (int i = 0; i < kCount; ++i) { + callback(x, y); + int state = i % 3; + if (state == 0) { + x++; + } else if (state == 1) { + y++; + } else { + x++; + y++; + } + } + } +}; + class All : public OldStyleSuiteSpecification { public: All() : OldStyleSuiteSpecification("DocumentSourceGroupTests") {} @@ -911,6 +1217,14 @@ public: add<Dependencies>(); add<StringConstantIdAndAccumulatorExpressions>(); add<ArrayConstantAccumulatorExpression>(); + + add<StreamingSimple>(); + add<WithoutStreamingSpills>(); + add<StreamingDoesNotSpill>(); + add<StreamingCanSpill>(); + add<StreamingAlternatingSpillAndNoSpillBatches>(); + add<StreamingComplex>(); + add<StreamingMultipleMonotonicFields>(); #if 0 // Disabled tests until SERVER-23318 is implemented. add<StreamingOptimization>(); diff --git a/src/mongo/db/pipeline/document_source_index_stats.cpp b/src/mongo/db/pipeline/document_source_index_stats.cpp index e87b614e59a..139fdde3bca 100644 --- a/src/mongo/db/pipeline/document_source_index_stats.cpp +++ b/src/mongo/db/pipeline/document_source_index_stats.cpp @@ -51,7 +51,10 @@ const char* DocumentSourceIndexStats::getSourceName() const { DocumentSource::GetNextResult DocumentSourceIndexStats::doGetNext() { if (_indexStats.empty()) { _indexStats = pExpCtx->mongoProcessInterface->getIndexStats( - pExpCtx->opCtx, pExpCtx->ns, _processName, pExpCtx->fromMongos); + pExpCtx->opCtx, + pExpCtx->ns, + _processName, + serverGlobalParams.clusterRole != ClusterRole::None); _indexStatsIter = _indexStats.cbegin(); } diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp new file mode 100644 index 00000000000..6c9693e0443 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.cpp @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/document_source_internal_all_collection_stats.h" + +namespace mongo { + +using boost::intrusive_ptr; + +DocumentSourceInternalAllCollectionStats::DocumentSourceInternalAllCollectionStats( + const boost::intrusive_ptr<ExpressionContext>& pExpCtx, + DocumentSourceInternalAllCollectionStatsSpec spec) + : DocumentSource(kStageNameInternal, pExpCtx), + _internalAllCollectionStatsSpec(std::move(spec)) {} + +REGISTER_DOCUMENT_SOURCE(_internalAllCollectionStats, + DocumentSourceInternalAllCollectionStats::LiteParsed::parse, + DocumentSourceInternalAllCollectionStats::createFromBsonInternal, + AllowedWithApiStrict::kInternal); + +DocumentSource::GetNextResult DocumentSourceInternalAllCollectionStats::doGetNext() { + if (!_catalogDocs) { + _catalogDocs = pExpCtx->mongoProcessInterface->listCatalog(pExpCtx->opCtx); + } + + while (!_catalogDocs->empty()) { + BSONObj obj(std::move(_catalogDocs->front())); + NamespaceString nss(obj["ns"].String()); + + _catalogDocs->pop_front(); + + // Avoid computing stats for collections that do not match the absorbed filter on the 'ns' + // field. + if (_absorbedMatch && !_absorbedMatch->getMatchExpression()->matchesBSON(std::move(obj))) { + continue; + } + + if (const auto& stats = _internalAllCollectionStatsSpec.getStats()) { + try { + return { + Document{DocumentSourceCollStats::makeStatsForNs(pExpCtx, nss, stats.get())}}; + } catch (const ExceptionFor<ErrorCodes::CommandNotSupportedOnView>&) { + // We don't want to retrieve data for views, only for collections. + continue; + } + } + } + + return GetNextResult::makeEOF(); +} + +Pipeline::SourceContainer::iterator DocumentSourceInternalAllCollectionStats::doOptimizeAt( + Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { + invariant(*itr == this); + + if (std::next(itr) == container->end()) { + return container->end(); + } + + // Attempt to internalize any predicates of a $match upon the "ns" field. + auto nextMatch = dynamic_cast<DocumentSourceMatch*>((*std::next(itr)).get()); + + if (!nextMatch) { + return std::next(itr); + } + + auto splitMatch = std::move(*nextMatch).splitSourceBy({"ns"}, {}); + invariant(splitMatch.first || splitMatch.second); + + // Remove the original $match. + container->erase(std::next(itr)); + + // Absorb the part of $match that is dependant on 'ns' + if (splitMatch.second) { + if (!_absorbedMatch) { + _absorbedMatch = std::move(splitMatch.second); + } else { + // We have already absorbed a $match. We need to join it with splitMatch.second. + _absorbedMatch->joinMatchWith(std::move(splitMatch.second)); + } + } + + // splitMatch.first is independent of 'ns'. Put it back on the pipeline. + if (splitMatch.first) { + container->insert(std::next(itr), std::move(splitMatch.first)); + return std::next(itr); + } else { + // There may be further optimization between this stage and the new neighbor, so we return + // an iterator pointing to ourself. + return itr; + } +} + +void DocumentSourceInternalAllCollectionStats::serializeToArray( + std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { + if (explain) { + BSONObjBuilder bob; + _internalAllCollectionStatsSpec.serialize(&bob); + if (_absorbedMatch) { + bob.append("match", _absorbedMatch->getQuery()); + } + auto doc = Document{{getSourceName(), bob.obj()}}; + array.push_back(Value(doc)); + } else { + array.push_back(serialize(explain)); + if (_absorbedMatch) { + _absorbedMatch->serializeToArray(array); + } + } +} + +intrusive_ptr<DocumentSource> DocumentSourceInternalAllCollectionStats::createFromBsonInternal( + BSONElement elem, const intrusive_ptr<ExpressionContext>& pExpCtx) { + uassert(6789103, + str::stream() << "$_internalAllCollectionStats must take a nested object but found: " + << elem, + elem.type() == BSONType::Object); + + uassert(6789104, + "The $_internalAllCollectionStats stage must be run on the admin database", + pExpCtx->ns.isAdminDB() && pExpCtx->ns.isCollectionlessAggregateNS()); + + auto spec = DocumentSourceInternalAllCollectionStatsSpec::parse( + IDLParserErrorContext(kStageNameInternal), elem.embeddedObject()); + + return make_intrusive<DocumentSourceInternalAllCollectionStats>(pExpCtx, std::move(spec)); +} + +const char* DocumentSourceInternalAllCollectionStats::getSourceName() const { + return kStageNameInternal.rawData(); +} + +Value DocumentSourceInternalAllCollectionStats::serialize( + boost::optional<ExplainOptions::Verbosity> explain) const { + return Value(Document{{getSourceName(), _internalAllCollectionStatsSpec.toBSON()}}); +} +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h new file mode 100644 index 00000000000..38324fb51ad --- /dev/null +++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.h @@ -0,0 +1,127 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/pipeline/document_source.h" +#include "mongo/db/pipeline/document_source_coll_stats.h" +#include "mongo/db/pipeline/document_source_internal_all_collection_stats_gen.h" +#include "mongo/db/pipeline/document_source_match.h" + +namespace mongo { + +/** + * This aggregation stage is the ‘$_internalAllCollectionStats´. It takes no arguments. Its + * response will be a cursor, each document of which represents the collection statistics for a + * single collection for all the existing collections. + * + * When executing the '$_internalAllCollectionStats' aggregation stage, we will need to obtain the + * catalog containing all collections namespaces. + * + * Then, for each collection, we will call `makeStatsForNs` method from DocumentSourceCollStats that + * will retrieve all storage stats for that particular collection. + */ +class DocumentSourceInternalAllCollectionStats final : public DocumentSource { +public: + static constexpr StringData kStageNameInternal = "$_internalAllCollectionStats"_sd; + + DocumentSourceInternalAllCollectionStats(const boost::intrusive_ptr<ExpressionContext>& pExpCtx, + DocumentSourceInternalAllCollectionStatsSpec spec); + + class LiteParsed final : public LiteParsedDocumentSource { + public: + static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, + const BSONElement& spec) { + return std::make_unique<LiteParsed>(spec.fieldName()); + } + + explicit LiteParsed(std::string parseTimeName) + : LiteParsedDocumentSource(std::move(parseTimeName)) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return stdx::unordered_set<NamespaceString>(); + } + + PrivilegeVector requiredPrivileges(bool isMongos, + bool bypassDocumentValidation) const final { + return { + Privilege(ResourcePattern::forClusterResource(), ActionType::allCollectionStats)}; + } + + bool isInitialSource() const final { + return true; + } + }; + + const char* getSourceName() const final; + + Value serialize(boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; + + StageConstraints constraints(Pipeline::SplitState pipeState) const final { + StageConstraints constraints(StreamType::kStreaming, + PositionRequirement::kFirst, + HostTypeRequirement::kAnyShard, + DiskUseRequirement::kNoDiskUse, + FacetRequirement::kNotAllowed, + TransactionRequirement::kNotAllowed, + LookupRequirement::kAllowed, + UnionRequirement::kAllowed); + + constraints.isIndependentOfAnyCollection = true; + constraints.requiresInputDocSource = false; + return constraints; + } + + boost::optional<DistributedPlanLogic> distributedPlanLogic() final { + return boost::none; + } + + static boost::intrusive_ptr<DocumentSource> createFromBsonInternal( + BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& pExpCtx); + + Pipeline::SourceContainer::iterator doOptimizeAt(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container) final; + + void serializeToArray( + std::vector<Value>& array, + boost::optional<ExplainOptions::Verbosity> explain = boost::none) const final; + +private: + GetNextResult doGetNext() final; + + // The specification object given to $_internalAllCollectionStats containing user specified + // options. + const DocumentSourceInternalAllCollectionStatsSpec _internalAllCollectionStatsSpec; + boost::optional<std::deque<BSONObj>> _catalogDocs; + + // A $match stage can be absorbed in order to avoid unnecessarily computing the stats for + // collections that do not match that predicate. + boost::intrusive_ptr<DocumentSourceMatch> _absorbedMatch; +}; +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl new file mode 100644 index 00000000000..61fc5d02f03 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_internal_all_collection_stats.idl @@ -0,0 +1,47 @@ +# Copyright (C) 2022-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo" + cpp_includes: + - "mongo/db/pipeline/document_source_parsing_validators.h" + +imports: + - "mongo/idl/basic_types.idl" + - "mongo/db/pipeline/storage_stats_spec.idl" + - "mongo/db/pipeline/document_source_coll_stats.idl" + +structs: + DocumentSourceInternalAllCollectionStatsSpec: + description: Specification for an $_internalAllCollectionStats stage. + strict: true + fields: + stats: + description: Specification for a $collStats stage. + type: DocumentSourceCollStatsSpec + optional: true diff --git a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h index 1083d58bb7f..b1a24787224 100644 --- a/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h +++ b/src/mongo/db/pipeline/document_source_internal_compute_geo_near_distance.h @@ -76,9 +76,7 @@ public: } DocumentSource::GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, - std::set<std::string>{_distanceField.fullPath()}, - {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{_distanceField.fullPath()}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() override { diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp index 272d7a1a80c..1c0dce7ef34 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.cpp @@ -29,6 +29,7 @@ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery +#include "mongo/db/pipeline/document_source_sequential_document_cache.h" #include <algorithm> #include <iterator> @@ -55,6 +56,7 @@ #include "mongo/db/pipeline/document_source_sample.h" #include "mongo/db/pipeline/document_source_single_document_transformation.h" #include "mongo/db/pipeline/document_source_sort.h" +#include "mongo/db/pipeline/document_source_streaming_group.h" #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/query/query_planner_common.h" @@ -243,9 +245,39 @@ DocumentSourceInternalUnpackBucket::DocumentSourceInternalUnpackBucket( bool assumeNoMixedSchemaData) : DocumentSource(kStageNameInternal, expCtx), _assumeNoMixedSchemaData(assumeNoMixedSchemaData), + _bucketUnpacker(std::move(bucketUnpacker)), _bucketMaxSpanSeconds{bucketMaxSpanSeconds} {} +DocumentSourceInternalUnpackBucket::DocumentSourceInternalUnpackBucket( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + BucketUnpacker bucketUnpacker, + int bucketMaxSpanSeconds, + const boost::optional<BSONObj>& eventFilterBson, + const boost::optional<BSONObj>& wholeBucketFilterBson, + bool assumeNoMixedSchemaData) + : DocumentSourceInternalUnpackBucket( + expCtx, std::move(bucketUnpacker), bucketMaxSpanSeconds, assumeNoMixedSchemaData) { + if (eventFilterBson) { + _eventFilterBson = eventFilterBson->getOwned(); + _eventFilter = + uassertStatusOK(MatchExpressionParser::parse(_eventFilterBson, + pExpCtx, + ExtensionsCallbackNoop(), + Pipeline::kAllowedMatcherFeatures)); + _eventFilterDeps = {}; + _eventFilter->addDependencies(&_eventFilterDeps); + } + if (wholeBucketFilterBson) { + _wholeBucketFilterBson = wholeBucketFilterBson->getOwned(); + _wholeBucketFilter = + uassertStatusOK(MatchExpressionParser::parse(_wholeBucketFilterBson, + pExpCtx, + ExtensionsCallbackNoop(), + Pipeline::kAllowedMatcherFeatures)); + } +} + boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createFromBsonInternal( BSONElement specElem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { uassert(5346500, @@ -255,14 +287,20 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF // If neither "include" nor "exclude" is specified, the default is "exclude": [] and // if that's the case, no field will be added to 'bucketSpec.fieldSet' in the for-loop below. - BucketUnpacker::Behavior unpackerBehavior = BucketUnpacker::Behavior::kExclude; BucketSpec bucketSpec; + // Use extended-range support if any individual collection requires it, even if 'specElem' + // doesn't mention this flag. + if (expCtx->getRequiresTimeseriesExtendedRangeSupport()) { + bucketSpec.setUsesExtendedRange(true); + } auto hasIncludeExclude = false; auto hasTimeField = false; auto hasBucketMaxSpanSeconds = false; auto bucketMaxSpanSeconds = 0; auto assumeClean = false; std::vector<std::string> computedMetaProjFields; + boost::optional<BSONObj> eventFilterBson; + boost::optional<BSONObj> wholeBucketFilterBson; for (auto&& elem : specElem.embeddedObject()) { auto fieldName = elem.fieldNameStringData(); if (fieldName == kInclude || fieldName == kExclude) { @@ -287,8 +325,8 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF field.find('.') == std::string::npos); bucketSpec.addIncludeExcludeField(field); } - unpackerBehavior = fieldName == kInclude ? BucketUnpacker::Behavior::kInclude - : BucketUnpacker::Behavior::kExclude; + bucketSpec.setBehavior(fieldName == kInclude ? BucketSpec::Behavior::kInclude + : BucketSpec::Behavior::kExclude); hasIncludeExclude = true; } else if (fieldName == kAssumeNoMixedSchemaData) { uassert(6067202, @@ -350,6 +388,24 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF << " field must be a bool, got: " << elem.type(), elem.type() == BSONType::Bool); bucketSpec.includeMaxTimeAsMetadata = elem.boolean(); + } else if (fieldName == kUsesExtendedRange) { + uassert(6646901, + str::stream() << kUsesExtendedRange + << " field must be a bool, got: " << elem.type(), + elem.type() == BSONType::Bool); + bucketSpec.setUsesExtendedRange(elem.boolean()); + } else if (fieldName == kEventFilter) { + uassert(7026902, + str::stream() << kEventFilter + << " field must be an object, got: " << elem.type(), + elem.type() == BSONType::Object); + eventFilterBson = elem.Obj(); + } else if (fieldName == kWholeBucketFilter) { + uassert(7026903, + str::stream() << kWholeBucketFilter + << " field must be an object, got: " << elem.type(), + elem.type() == BSONType::Object); + wholeBucketFilterBson = elem.Obj(); } else { uasserted(5346506, str::stream() @@ -364,11 +420,12 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF "The $_internalUnpackBucket stage requires a bucketMaxSpanSeconds parameter", hasBucketMaxSpanSeconds); - return make_intrusive<DocumentSourceInternalUnpackBucket>( - expCtx, - BucketUnpacker{std::move(bucketSpec), unpackerBehavior}, - bucketMaxSpanSeconds, - assumeClean); + return make_intrusive<DocumentSourceInternalUnpackBucket>(expCtx, + BucketUnpacker{std::move(bucketSpec)}, + bucketMaxSpanSeconds, + eventFilterBson, + wholeBucketFilterBson, + assumeClean); } boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createFromBsonExternal( @@ -414,26 +471,23 @@ boost::intrusive_ptr<DocumentSource> DocumentSourceInternalUnpackBucket::createF hasTimeField); return make_intrusive<DocumentSourceInternalUnpackBucket>( - expCtx, - BucketUnpacker{std::move(bucketSpec), BucketUnpacker::Behavior::kExclude}, - 3600, - assumeClean); + expCtx, BucketUnpacker{std::move(bucketSpec)}, 3600, assumeClean); } void DocumentSourceInternalUnpackBucket::serializeToArray( std::vector<Value>& array, boost::optional<ExplainOptions::Verbosity> explain) const { MutableDocument out; auto behavior = - _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kInclude ? kInclude : kExclude; + _bucketUnpacker.behavior() == BucketSpec::Behavior::kInclude ? kInclude : kExclude; const auto& spec = _bucketUnpacker.bucketSpec(); std::vector<Value> fields; for (auto&& field : spec.fieldSet()) { fields.emplace_back(field); } if (((_bucketUnpacker.includeMetaField() && - _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kInclude) || + _bucketUnpacker.behavior() == BucketSpec::Behavior::kInclude) || (!_bucketUnpacker.includeMetaField() && - _bucketUnpacker.behavior() == BucketUnpacker::Behavior::kExclude && spec.metaField())) && + _bucketUnpacker.behavior() == BucketSpec::Behavior::kExclude && spec.metaField())) && std::find(spec.computedMetaProjFields().cbegin(), spec.computedMetaProjFields().cend(), *spec.metaField()) == spec.computedMetaProjFields().cend()) @@ -448,6 +502,14 @@ void DocumentSourceInternalUnpackBucket::serializeToArray( if (_assumeNoMixedSchemaData) out.addField(kAssumeNoMixedSchemaData, Value(_assumeNoMixedSchemaData)); + if (spec.usesExtendedRange()) { + // Include this flag so that 'explain' is more helpful. + // But this is not so useful for communicating from one process to another, + // because mongos and/or the primary shard don't know whether any other shard + // has extended-range data. + out.addField(kUsesExtendedRange, Value{true}); + } + if (!spec.computedMetaProjFields().empty()) out.addField("computedMetaProjFields", Value{[&] { std::vector<Value> compFields; @@ -465,6 +527,13 @@ void DocumentSourceInternalUnpackBucket::serializeToArray( out.addField(kIncludeMaxTimeAsMetadata, Value{_bucketUnpacker.includeMaxTimeAsMetadata()}); } + if (_wholeBucketFilter) { + out.addField(kWholeBucketFilter, Value{_wholeBucketFilter->serialize()}); + } + if (_eventFilter) { + out.addField(kEventFilter, Value{_eventFilter->serialize()}); + } + if (!explain) { array.push_back(Value(DOC(getSourceName() << out.freeze()))); if (_sampleSize) { @@ -480,25 +549,58 @@ void DocumentSourceInternalUnpackBucket::serializeToArray( } } +boost::optional<Document> DocumentSourceInternalUnpackBucket::getNextMatchingMeasure() { + while (_bucketUnpacker.hasNext()) { + if (_eventFilter) { + if (_unpackToBson) { + auto measure = _bucketUnpacker.getNextBson(); + if (_bucketUnpacker.bucketMatchedQuery() || _eventFilter->matchesBSON(measure)) { + return Document(measure); + } + } else { + auto measure = _bucketUnpacker.getNext(); + // MatchExpression only takes BSON documents, so we have to make one. As an + // optimization, only serialize the fields we need to do the match. + BSONObj measureBson = _eventFilterDeps.needWholeDocument + ? measure.toBson() + : document_path_support::documentToBsonWithPaths(measure, + _eventFilterDeps.fields); + if (_bucketUnpacker.bucketMatchedQuery() || + _eventFilter->matchesBSON(measureBson)) { + return measure; + } + } + } else { + return _bucketUnpacker.getNext(); + } + } + return {}; +} + DocumentSource::GetNextResult DocumentSourceInternalUnpackBucket::doGetNext() { tassert(5521502, "calling doGetNext() when '_sampleSize' is set is disallowed", !_sampleSize); // Otherwise, fallback to unpacking every measurement in all buckets until the child stage is // exhausted. - if (_bucketUnpacker.hasNext()) { - return _bucketUnpacker.getNext(); + if (auto measure = getNextMatchingMeasure()) { + return GetNextResult(std::move(*measure)); } auto nextResult = pSource->getNext(); - if (nextResult.isAdvanced()) { + while (nextResult.isAdvanced()) { auto bucket = nextResult.getDocument().toBson(); - _bucketUnpacker.reset(std::move(bucket)); + auto bucketMatchedQuery = _wholeBucketFilter && _wholeBucketFilter->matchesBSON(bucket); + _bucketUnpacker.reset(std::move(bucket), bucketMatchedQuery); + uassert(5346509, str::stream() << "A bucket with _id " << _bucketUnpacker.bucket()[timeseries::kBucketIdFieldName].toString() << " contains an empty data region", _bucketUnpacker.hasNext()); - return _bucketUnpacker.getNext(); + if (auto measure = getNextMatchingMeasure()) { + return GetNextResult(std::move(*measure)); + } + nextResult = pSource->getNext(); } return nextResult; @@ -510,7 +612,7 @@ bool DocumentSourceInternalUnpackBucket::pushDownComputedMetaProjection( if (std::next(itr) == container->end()) { return nextStageWasRemoved; } - if (!_bucketUnpacker.bucketSpec().metaField()) { + if (!_bucketUnpacker.getMetaField() || !_bucketUnpacker.includeMetaField()) { return nextStageWasRemoved; } @@ -562,9 +664,8 @@ void DocumentSourceInternalUnpackBucket::internalizeProject(const BSONObj& proje // Update '_bucketUnpacker' state with the new fields and behavior. auto spec = _bucketUnpacker.bucketSpec(); spec.setFieldSet(fields); - _bucketUnpacker.setBucketSpecAndBehavior(std::move(spec), - isInclusion ? BucketUnpacker::Behavior::kInclude - : BucketUnpacker::Behavior::kExclude); + spec.setBehavior(isInclusion ? BucketSpec::Behavior::kInclude : BucketSpec::Behavior::kExclude); + _bucketUnpacker.setBucketSpec(std::move(spec)); } std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProjectToInternalize( @@ -576,7 +677,8 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje // Check for a viable inclusion $project after the $_internalUnpackBucket. auto [existingProj, isInclusion] = getIncludeExcludeProjectAndType(std::next(itr)->get()); - if (isInclusion && !existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) { + if (!_eventFilter && isInclusion && !existingProj.isEmpty() && + canInternalizeProjectObj(existingProj)) { container->erase(std::next(itr)); return {existingProj, isInclusion}; } @@ -584,8 +686,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje // Attempt to get an inclusion $project representing the root-level dependencies of the pipeline // after the $_internalUnpackBucket. If this $project is not empty, then the dependency set was // finite. - Pipeline::SourceContainer restOfPipeline(std::next(itr), container->end()); - auto deps = Pipeline::getDependenciesForContainer(pExpCtx, restOfPipeline, boost::none); + auto deps = getRestPipelineDependencies(itr, container, true /* includeEventFilter */); if (auto dependencyProj = deps.toProjectionWithoutMetadata(DepsTracker::TruncateToRootLevel::yes); !dependencyProj.isEmpty()) { @@ -593,7 +694,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje } // Check for a viable exclusion $project after the $_internalUnpackBucket. - if (!existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) { + if (!_eventFilter && !existingProj.isEmpty() && canInternalizeProjectObj(existingProj)) { container->erase(std::next(itr)); return {existingProj, isInclusion}; } @@ -601,8 +702,7 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractOrBuildProje return {BSONObj{}, false}; } -std::unique_ptr<MatchExpression> -DocumentSourceInternalUnpackBucket::createPredicatesOnBucketLevelField( +BucketSpec::BucketPredicate DocumentSourceInternalUnpackBucket::createPredicatesOnBucketLevelField( const MatchExpression* matchExpr) const { return BucketSpec::createPredicatesOnBucketLevelField( matchExpr, @@ -631,58 +731,102 @@ std::pair<BSONObj, bool> DocumentSourceInternalUnpackBucket::extractProjectForPu std::pair<bool, Pipeline::SourceContainer::iterator> DocumentSourceInternalUnpackBucket::rewriteGroupByMinMax(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { + // The computed min/max for each bucket uses the default collation. If the collation of the + // query doesn't match the default we cannot rely on the computed values as they might differ + // (e.g. numeric and lexicographic collations compare "5" and "10" in opposite order). + // NB: Unfortuntealy, this means we have to forgo the optimization even if the source field is + // numeric and not affected by the collation as we cannot know the data type until runtime. + if (pExpCtx->collationMatchesDefault == ExpressionContext::CollationMatchesDefault::kNo) { + return {}; + } + const auto* groupPtr = dynamic_cast<DocumentSourceGroup*>(std::next(itr)->get()); if (groupPtr == nullptr) { return {}; } + if (!_bucketUnpacker.bucketSpec().metaField()) { + return {}; + } + const auto& metaField = *_bucketUnpacker.bucketSpec().metaField(); + const auto& idFields = groupPtr->getIdFields(); - if (idFields.size() != 1 || !_bucketUnpacker.bucketSpec().metaField().has_value()) { + + // Currently, we only support simple group key. TODO: SERVER-68811. Allow rewrites of object + // group key if all its fields depend on the metaField only. + if (idFields.size() != 1) { return {}; } const auto& exprId = idFields.cbegin()->second; const auto* exprIdPath = dynamic_cast<const ExpressionFieldPath*>(exprId.get()); + + // Currently, we only support group key expression of the form {_id : "<path>"}. + // TODO: SERVER-68811. Allow rewrites if expression is constant. if (exprIdPath == nullptr) { return {}; } const auto& idPath = exprIdPath->getFieldPath(); - if (idPath.getPathLength() < 2 || - idPath.getFieldName(1) != _bucketUnpacker.bucketSpec().metaField().get()) { + // {The path must be at or under the metaField for this re-write to be correct (and the zero + // component is always CURRENT).} + if (idPath.getPathLength() < 2 || idPath.getFieldName(1) != metaField) { return {}; } - bool suitable = true; - std::vector<AccumulationStatement> accumulationStatements; + std::vector<AccumulationStatement> accumulationStatementsBucket; for (const AccumulationStatement& stmt : groupPtr->getAccumulatedFields()) { - const auto op = stmt.expr.name; - const bool isMin = op == "$min"; - const bool isMax = op == "$max"; + const auto& op = stmt.expr.name; + // If _any_ of the accumulators aren't $min/$max we won't perform the re-write (some other + // accs might be re-writable in terms of the bucket controls, we just haven't invested into + // implementing them). + if (op != "$min" && op != "$max") { + return {}; + } - // Rewrite is valid only for min and max aggregates. - if (!isMin && !isMax) { - suitable = false; - break; + const auto* exprArgPath = + dynamic_cast<const ExpressionFieldPath*>(stmt.expr.argument.get()); + + // This is either a const or a compound expression. While some such expressions (e.g: {$min: + // {$add: + // ['$a', 2]}}) could be re-written in terms of the min/max on the control fields, in + // general we cannot do it (e.g. {$min: {$add: ['$a', '$b']}}), so we block the re-write. + if (!exprArgPath) { + return {}; } - const auto* exprArg = stmt.expr.argument.get(); - if (const auto* exprArgPath = dynamic_cast<const ExpressionFieldPath*>(exprArg)) { - const auto& path = exprArgPath->getFieldPath(); - if (path.getPathLength() <= 1 || - path.getFieldName(1) == _bucketUnpacker.bucketSpec().timeField()) { - // Rewrite not valid for time field. We want to eliminate the bucket - // unpack stage here. - suitable = false; - break; - } + // Path can have a single component if it's using $$CURRENT or a similar variable. We don't + // support these. + const auto& path = exprArgPath->getFieldPath(); + if (path.getPathLength() <= 1) { + return {}; + } + const auto& accFieldName = path.getFieldName(1); + + // Rewrite not valid for the timeField because control.min.time contains a rounded-down time + // and not the actual min time of events in the bucket. + if (accFieldName == _bucketUnpacker.bucketSpec().timeField()) { + return {}; + } + // Build the paths for the bucket-level fields. + std::ostringstream os; + if (accFieldName == metaField) { + // Update aggregates to reference the meta field. + os << timeseries::kBucketMetaFieldName; + + for (size_t index = 2; index < path.getPathLength(); index++) { + os << "." << path.getFieldName(index); + } + } else { // Update aggregates to reference the control field. - std::ostringstream os; - if (isMin) { + const auto op = stmt.expr.name; + if (op == "$min") { os << timeseries::kControlMinFieldNamePrefix; - } else { + } else if (op == "$max") { os << timeseries::kControlMaxFieldNamePrefix; + } else { + MONGO_UNREACHABLE; } for (size_t index = 1; index < path.getPathLength(); index++) { @@ -691,45 +835,43 @@ DocumentSourceInternalUnpackBucket::rewriteGroupByMinMax(Pipeline::SourceContain } os << path.getFieldName(index); } - - const auto& newExpr = ExpressionFieldPath::createPathFromString( - pExpCtx.get(), os.str(), pExpCtx->variablesParseState); - - AccumulationExpression accExpr = stmt.expr; - accExpr.argument = newExpr; - accumulationStatements.emplace_back(stmt.fieldName, std::move(accExpr)); } - } - if (suitable) { - std::ostringstream os; - os << timeseries::kBucketMetaFieldName; - for (size_t index = 2; index < idPath.getPathLength(); index++) { - os << "." << idPath.getFieldName(index); - } - auto exprId1 = ExpressionFieldPath::createPathFromString( + // Re-create the accumulator using the bucket-level paths. + const auto& newExpr = ExpressionFieldPath::createPathFromString( pExpCtx.get(), os.str(), pExpCtx->variablesParseState); - auto newGroup = DocumentSourceGroup::create(pExpCtx, - std::move(exprId1), - std::move(accumulationStatements), - groupPtr->getMaxMemoryUsageBytes()); - - // Erase current stage and following group stage, and replace with updated - // group. - container->erase(std::next(itr)); - *itr = std::move(newGroup); - - if (itr == container->begin()) { - // Optimize group stage. - return {true, itr}; - } else { - // Give chance of the previous stage to optimize against group stage. - return {true, std::prev(itr)}; - } + AccumulationExpression accExpr = stmt.expr; + accExpr.argument = newExpr; + accumulationStatementsBucket.emplace_back(stmt.fieldName, std::move(accExpr)); } - return {}; + // Re-create the group key using the bucket-level path. + std::ostringstream os; + os << timeseries::kBucketMetaFieldName; + for (size_t index = 2; index < idPath.getPathLength(); index++) { + os << "." << idPath.getFieldName(index); + } + auto exprIdBucket = ExpressionFieldPath::createPathFromString( + pExpCtx.get(), os.str(), pExpCtx->variablesParseState); + + auto newGroup = DocumentSourceGroup::create(pExpCtx, + std::move(exprIdBucket), + std::move(accumulationStatementsBucket), + groupPtr->getMaxMemoryUsageBytes()); + + // Replace the current stage (DocumentSourceInternalUnpackBucket) and the following group stage + // with the new group. + container->erase(std::next(itr)); + *itr = std::move(newGroup); + + if (itr == container->begin()) { + // Optimize the new group stage. + return {true, itr}; + } else { + // Give chance to the previous stage to optimize against the new group stage. + return {true, std::prev(itr)}; + } } bool DocumentSourceInternalUnpackBucket::haveComputedMetaField() const { @@ -738,6 +880,68 @@ bool DocumentSourceInternalUnpackBucket::haveComputedMetaField() const { _bucketUnpacker.bucketSpec().metaField().get()); } +bool DocumentSourceInternalUnpackBucket::enableStreamingGroupIfPossible( + Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { + // skip unpack stage + itr = std::next(itr); + + FieldPath timeField = _bucketUnpacker.bucketSpec().timeField(); + DocumentSourceGroup* groupStage = nullptr; + bool isSortedOnTime = false; + for (; itr != container->end(); ++itr) { + if (auto groupStagePtr = dynamic_cast<DocumentSourceGroup*>(itr->get())) { + groupStage = groupStagePtr; + break; + } + if (auto sortStagePtr = dynamic_cast<DocumentSourceSort*>(itr->get())) { + isSortedOnTime = sortStagePtr->getSortKeyPattern().front().fieldPath == timeField; + } else if (!itr->get()->constraints().preservesOrderAndMetadata) { + // If this is after the sort, the sort is invalidated. If it's before the sort, there's + // no harm in keeping the boolean false. + isSortedOnTime = false; + } + // We modify time field, so we can't proceed with optimization. It may be possible to + // proceed in some cases if the modification happens before the sort, but we won't worry + // about or bother with those - in large part because it is risky that it will change the + // type away from a date into something with more difficult/subtle semantics. + if (itr->get()->getModifiedPaths().canModify(timeField)) { + return false; + } + } + + if (groupStage == nullptr || !isSortedOnTime) { + return false; + } + + const auto& idFields = groupStage->getMutableIdFields(); + std::vector<size_t> monotonicIdFields; + for (size_t i = 0; i < idFields.size(); ++i) { + // To enable streaming, we need id field expression to be clustered, so that all documents + // with the same value of this id field are in a single continious cluster. However this + // property is hard to check for, so we check for monotonicity instead, which is stronger. + idFields[i]->optimize(); // We optimize here to make use of constant folding. + auto monotonicState = idFields[i]->getMonotonicState(timeField); + + // We don't add monotonic::State::Constant id fields, because they are useless when + // determining if a group batch is finished. + if (monotonicState == monotonic::State::Increasing || + monotonicState == monotonic::State::Decreasing) { + monotonicIdFields.push_back(i); + } + } + if (monotonicIdFields.empty()) { + return false; + } + + *itr = + DocumentSourceStreamingGroup::create(pExpCtx, + groupStage->getIdExpression(), + std::move(monotonicIdFields), + std::move(groupStage->getMutableAccumulatedFields()), + groupStage->getMaxMemoryUsageBytes()); + return true; +} + template <TopBottomSense sense, bool single> bool extractFromAcc(const AccumulatorN* acc, const boost::intrusive_ptr<Expression>& init, @@ -980,6 +1184,27 @@ bool DocumentSourceInternalUnpackBucket::optimizeLastpoint(Pipeline::SourceConta tryInsertBucketLevelSortAndGroup(AccumulatorDocumentsNeeded::kLastDocument); } + +bool findSequentialDocumentCache(Pipeline::SourceContainer::iterator start, + Pipeline::SourceContainer::iterator end) { + while (start != end && !dynamic_cast<DocumentSourceSequentialDocumentCache*>(start->get())) { + start = std::next(start); + } + return start != end; +} + +DepsTracker DocumentSourceInternalUnpackBucket::getRestPipelineDependencies( + Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container, + bool includeEventFilter) const { + auto deps = Pipeline::getDependenciesForContainer( + pExpCtx, Pipeline::SourceContainer{std::next(itr), container->end()}, boost::none); + if (_eventFilter && includeEventFilter) { + _eventFilter->addDependencies(&deps); + } + return deps; +} + Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimizeAt( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { invariant(*itr == this); @@ -993,7 +1218,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi bool haveComputedMetaField = this->haveComputedMetaField(); // Before any other rewrites for the current stage, consider reordering with $sort. - if (auto sortPtr = dynamic_cast<DocumentSourceSort*>(std::next(itr)->get())) { + if (auto sortPtr = dynamic_cast<DocumentSourceSort*>(std::next(itr)->get()); + sortPtr && !_eventFilter) { if (auto metaField = _bucketUnpacker.bucketSpec().metaField(); metaField && !haveComputedMetaField) { if (checkMetadataSortReorder(sortPtr->getSortKeyPattern(), metaField.get())) { @@ -1024,7 +1250,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi } // Attempt to push geoNear on the metaField past $_internalUnpackBucket. - if (auto nextNear = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get())) { + if (auto nextNear = dynamic_cast<DocumentSourceGeoNear*>(std::next(itr)->get()); + nextNear && !_eventFilter) { // Currently we only support geo indexes on the meta field, and we enforce this by // requiring the key field to be set so we can check before we try to look up indexes. auto keyField = nextNear->getKeyField(); @@ -1073,8 +1300,19 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi // the first stage, because it expects to use a special DocumentSouceGeoNearCursor plan. nextStage->optimizeAt(std::next(itr), container); } + auto cacheFound = findSequentialDocumentCache(itr, container->end()); + if (cacheFound) { + // optimizeAt() is responsible for reordering stages, and optimize() is responsible for + // simplifying individual stages. $sequentialCache's optimizeAt() places the stage where + // it can cache as big a prefix of the pipeline as possible. To do so correctly, it + // needs to look at dependencies: a stage that depends on a let-variable cannot be + // cached. But optimize() can inline variables. Therefore, we want to avoid calling + // optimize() before $sequentialCache has a chance to run optimizeAt(). + return Pipeline::optimizeAtEndOfPipeline(itr, container); + } else { + Pipeline::optimizeEndOfPipeline(itr, container); + } - Pipeline::optimizeEndOfPipeline(itr, container); if (std::next(itr) == container->end()) { return container->end(); } else { @@ -1083,7 +1321,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi return itr; } } - { + + if (!_eventFilter) { // Check if we can avoid unpacking if we have a group stage with min/max aggregates. auto [success, result] = rewriteGroupByMinMax(itr, container); if (success) { @@ -1094,13 +1333,12 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi { // Check if the rest of the pipeline needs any fields. For example we might only be // interested in $count. - auto deps = Pipeline::getDependenciesForContainer( - pExpCtx, Pipeline::SourceContainer{std::next(itr), container->end()}, boost::none); + auto deps = getRestPipelineDependencies(itr, container, true /* includeEventFilter */); if (deps.hasNoRequirements()) { - _bucketUnpacker.setBucketSpecAndBehavior({_bucketUnpacker.bucketSpec().timeField(), - _bucketUnpacker.bucketSpec().metaField(), - {}}, - BucketUnpacker::Behavior::kInclude); + _bucketUnpacker.setBucketSpec({_bucketUnpacker.bucketSpec().timeField(), + _bucketUnpacker.bucketSpec().metaField(), + {}, + BucketSpec::Behavior::kInclude}); // Keep going for next optimization. } @@ -1117,31 +1355,72 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi // Attempt to optimize last-point type queries. if (feature_flags::gfeatureFlagLastPointQuery.isEnabled( serverGlobalParams.featureCompatibility) && - !_triedLastpointRewrite && optimizeLastpoint(itr, container)) { + !_triedLastpointRewrite && !_eventFilter && optimizeLastpoint(itr, container)) { _triedLastpointRewrite = true; // If we are able to rewrite the aggregation, give the resulting pipeline a chance to // perform further optimizations. return container->begin(); }; - // Attempt to map predicates on bucketed fields to predicates on the control field. - if (auto nextMatch = dynamic_cast<DocumentSourceMatch*>(std::next(itr)->get()); - nextMatch && !_triedBucketLevelFieldsPredicatesPushdown) { - _triedBucketLevelFieldsPredicatesPushdown = true; + // Attempt to map predicates on bucketed fields to the predicates on the control field. + if (auto nextMatch = dynamic_cast<DocumentSourceMatch*>(std::next(itr)->get())) { + + // Merge multiple following $match stages. + auto itrToMatch = std::next(itr); + while (std::next(itrToMatch) != container->end() && + dynamic_cast<DocumentSourceMatch*>(std::next(itrToMatch)->get())) { + nextMatch->doOptimizeAt(itrToMatch, container); + } + + auto predicates = createPredicatesOnBucketLevelField(nextMatch->getMatchExpression()); + + // Try to create a tight bucket predicate to perform bucket level matching. + if (predicates.tightPredicate) { + _wholeBucketFilterBson = predicates.tightPredicate->serialize(); + _wholeBucketFilter = + uassertStatusOK(MatchExpressionParser::parse(_wholeBucketFilterBson, + pExpCtx, + ExtensionsCallbackNoop(), + Pipeline::kAllowedMatcherFeatures)); + _wholeBucketFilter = MatchExpression::optimize(std::move(_wholeBucketFilter)); + } - if (auto match = createPredicatesOnBucketLevelField(nextMatch->getMatchExpression())) { + // Push the original event predicate into the unpacking stage. + _eventFilterBson = nextMatch->getQuery().getOwned(); + _eventFilter = + uassertStatusOK(MatchExpressionParser::parse(_eventFilterBson, + pExpCtx, + ExtensionsCallbackNoop(), + Pipeline::kAllowedMatcherFeatures)); + _eventFilter = MatchExpression::optimize(std::move(_eventFilter)); + _eventFilterDeps = {}; + _eventFilter->addDependencies(&_eventFilterDeps); + container->erase(std::next(itr)); + + // If the $match is not followed by other stages referencing fields (e.g. $count), we can + // unpack directly to BSON so that data doesn't need to be materialized to Document. + auto deps = getRestPipelineDependencies(itr, container, false /* includeEventFilter */); + if (deps.fields.empty()) { + _unpackToBson = true; + } + + // Create a loose bucket predicate and push it before the unpacking stage. + if (predicates.loosePredicate) { BSONObjBuilder bob; - match->serialize(&bob); + predicates.loosePredicate->serialize(&bob); container->insert(itr, DocumentSourceMatch::create(bob.obj(), pExpCtx)); // Give other stages a chance to optimize with the new $match. return std::prev(itr) == container->begin() ? std::prev(itr) : std::prev(std::prev(itr)); } + + // We have removed a $match after this stage, so we try to optimize this stage again. + return itr; } // Attempt to push down a $project on the metaField past $_internalUnpackBucket. - if (!haveComputedMetaField) { + if (!_eventFilter && !haveComputedMetaField) { if (auto [metaProject, deleteRemainder] = extractProjectForPushDown(std::next(itr)->get()); !metaProject.isEmpty()) { container->insert(itr, @@ -1160,7 +1439,7 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi // Attempt to extract computed meta projections from subsequent $project, $addFields, or $set // and push them before the $_internalunpackBucket. - if (pushDownComputedMetaProjection(itr, container)) { + if (!_eventFilter && pushDownComputedMetaProjection(itr, container)) { // We've pushed down and removed a stage after this one. Try to optimize the new stage. return std::prev(itr) == container->begin() ? std::prev(itr) : std::prev(std::prev(itr)); } @@ -1179,6 +1458,8 @@ Pipeline::SourceContainer::iterator DocumentSourceInternalUnpackBucket::doOptimi } } + enableStreamingGroupIfPossible(itr, container); + return container->end(); } @@ -1187,8 +1468,8 @@ DocumentSource::GetModPathsReturn DocumentSourceInternalUnpackBucket::getModifie StringMap<std::string> renames; renames.emplace(*_bucketUnpacker.bucketSpec().metaField(), timeseries::kBucketMetaFieldName); - return {GetModPathsReturn::Type::kAllExcept, std::set<std::string>{}, std::move(renames)}; + return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{}, std::move(renames)}; } - return {GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; } } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h index df3bf93bff4..4d20a22f107 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket.h @@ -47,9 +47,12 @@ public: static constexpr StringData kInclude = "include"_sd; static constexpr StringData kExclude = "exclude"_sd; static constexpr StringData kAssumeNoMixedSchemaData = "assumeNoMixedSchemaData"_sd; + static constexpr StringData kUsesExtendedRange = "usesExtendedRange"_sd; static constexpr StringData kBucketMaxSpanSeconds = "bucketMaxSpanSeconds"_sd; static constexpr StringData kIncludeMinTimeAsMetadata = "includeMinTimeAsMetadata"_sd; static constexpr StringData kIncludeMaxTimeAsMetadata = "includeMaxTimeAsMetadata"_sd; + static constexpr StringData kWholeBucketFilter = "wholeBucketFilter"_sd; + static constexpr StringData kEventFilter = "eventFilter"_sd; static boost::intrusive_ptr<DocumentSource> createFromBsonInternal( BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); @@ -61,6 +64,13 @@ public: int bucketMaxSpanSeconds, bool assumeNoMixedSchemaData = false); + DocumentSourceInternalUnpackBucket(const boost::intrusive_ptr<ExpressionContext>& expCtx, + BucketUnpacker bucketUnpacker, + int bucketMaxSpanSeconds, + const boost::optional<BSONObj>& eventFilterBson, + const boost::optional<BSONObj>& wholeBucketFilterBson, + bool assumeNoMixedSchemaData = false); + const char* getSourceName() const override { return kStageNameInternal.rawData(); } @@ -95,6 +105,8 @@ public: UnionRequirement::kAllowed, ChangeStreamRequirement::kDenylist}; constraints.canSwapWithMatch = true; + // The user cannot specify multiple $unpackBucket stages in the pipeline. + constraints.canAppearOnlyOnceInPipeline = true; return constraints; } @@ -155,7 +167,7 @@ public: /** * Convenience wrapper around BucketSpec::createPredicatesOnBucketLevelField(). */ - std::unique_ptr<MatchExpression> createPredicatesOnBucketLevelField( + BucketSpec::BucketPredicate createPredicatesOnBucketLevelField( const MatchExpression* matchExpr) const; /** @@ -209,13 +221,20 @@ public: /** * Helper method which checks if we can avoid unpacking if we have a group stage with min/max - * aggregates. If a rewrite is possible, 'container' is modified, and we returns result value - * for 'doOptimizeAt'. + * aggregates. If the rewrite is possible, 'container' is modified, bool in the return pair is + * set to 'true' and the iterator is set to point to the new group. */ std::pair<bool, Pipeline::SourceContainer::iterator> rewriteGroupByMinMax( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container); /** + * Helper method which checks if we can replace DocumentSourceGroup with + * DocumentSourceStreamingGroup. Returns true if the optimization is performed. + */ + bool enableStreamingGroupIfPossible(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container); + + /** * If the current aggregation is a lastpoint-type query (ie. with a $sort on meta and time * fields, and a $group with a meta _id and only $first or $last accumulators) we can rewrite * it to avoid unpacking all buckets. @@ -240,23 +259,48 @@ public: GetModPathsReturn getModifiedPaths() const final override; + DepsTracker getRestPipelineDependencies(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container, + bool includeEventFilter) const; + private: GetNextResult doGetNext() final; + + boost::optional<Document> getNextMatchingMeasure(); + bool haveComputedMetaField() const; // If buckets contained a mixed type schema along some path, we have to push down special // predicates in order to ensure correctness. bool _assumeNoMixedSchemaData = false; + // If any bucket contains dates outside the range of 1970-2038, we are unable to rely on + // the _id index, as _id is truncates to 32 bits + bool _usesExtendedRange = false; + BucketUnpacker _bucketUnpacker; int _bucketMaxSpanSeconds; int _bucketMaxCount = 0; boost::optional<long long> _sampleSize; - // Used to avoid infinite loops after we step backwards to optimize a $match on bucket level - // fields, otherwise we may do an infinite number of $match pushdowns. - bool _triedBucketLevelFieldsPredicatesPushdown = false; + // It's benefitial to do as much filtering at the bucket level as possible to avoid unpacking + // buckets that wouldn't contribute to the results anyway. There is a generic mechanism that + // allows to swap $match stages with this one (see 'getModifiedPaths()'). It lets us split out + // and push down a filter on the metaField "as is". The remaining filters might cause creation + // of additional bucket-level filters (see 'createPredicatesOnBucketLevelField()') that are + // inserted before this stage while the original filter is incorporated into this stage as + // '_eventFilter' (to be applied to each unpacked document) and/or '_wholeBucketFilter' for the + // cases when _all_ events in a bucket would match (currently, we only do this for the + // timeField). + std::unique_ptr<MatchExpression> _eventFilter; + BSONObj _eventFilterBson; + DepsTracker _eventFilterDeps; + std::unique_ptr<MatchExpression> _wholeBucketFilter; + BSONObj _wholeBucketFilterBson; + + bool _unpackToBson = false; + bool _optimizedEndOfPipeline = false; bool _triedInternalizeProject = false; bool _triedLastpointRewrite = false; diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp index ffe291dcbd0..63d3b4a0b23 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/create_predicates_on_bucket_level_field_test.cpp @@ -55,10 +55,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -76,10 +77,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -97,10 +99,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.min.a': {$_internalExprLt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -118,10 +121,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.min.a': {$_internalExprLte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -139,11 +143,12 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {$and:[{'control.min.a': {$_internalExprLte: 1}}," "{'control.max.a': {$_internalExprGte: 1}}]}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -161,7 +166,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate); + ASSERT(predicate.loosePredicate); auto expected = fromjson( "{$or: [" " {$or: [" @@ -185,7 +190,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}}" " ]}" "]}"); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), expected); + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), expected); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -208,10 +214,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -234,10 +241,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.max.a': {$_internalExprGte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -260,10 +268,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.min.a': {$_internalExprLt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -286,10 +295,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {'control.min.a': {$_internalExprLte: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -312,11 +322,12 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [ {$and:[{'control.min.a': {$_internalExprLte: 1}}," "{'control.max.a': {$_internalExprGte: 1}}]}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -334,13 +345,14 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$and: [ {$or: [ {'control.max.b': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]}," "{$type: [ \"$control.max.b\" ]} ]}} ]}," "{$or: [ {'control.min.a': {$_internalExprLt: 5}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -358,7 +370,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate == nullptr); + ASSERT(predicate.loosePredicate == nullptr); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -376,7 +389,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [" " {'control.max.b': {$_internalExprGt: 1}}," " {$expr: {$ne: [" @@ -384,6 +397,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " {$type: [ \"$control.max.b\" ]}" " ]}}" "]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -402,7 +416,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$and: [ {$or: [ {'control.max.b': {$_internalExprGte: 2}}," "{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]}," "{$type: [ \"$control.max.b\" ]} ]}} ]}," @@ -412,6 +426,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, "{$or: [ {'control.min.a': {$_internalExprLt: 5}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]} ]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -429,8 +444,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT(predicate.loosePredicate); + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [" " {$or: [" " {'control.max.b': {$_internalExprGt: 1}}," @@ -447,6 +462,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}}" " ]}" "]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -464,7 +480,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate == nullptr); + ASSERT(predicate.loosePredicate == nullptr); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -485,7 +502,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, // When a predicate can't be pushed down, it's the same as pushing down a trivially-true // predicate. So when any child of an $or can't be pushed down, we could generate something like // {$or: [ ... {$alwaysTrue: {}}, ... ]}, but then we might as well not push down the whole $or. - ASSERT(predicate == nullptr); + ASSERT(predicate.loosePredicate == nullptr); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -504,7 +522,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$or: [" " {$or: [" " {'control.max.b': {$_internalExprGte: 2}}," @@ -530,6 +548,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}" " ]}" "]}")); + ASSERT_FALSE(predicate.tightPredicate); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -541,19 +560,21 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ASSERT_EQ(pipeline->getSources().size(), 2U); pipeline->optimizePipeline(); - ASSERT_EQ(pipeline->getSources().size(), 3U); + ASSERT_EQ(pipeline->getSources().size(), 2U); // To get the optimized $match from the pipeline, we have to serialize with explain. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(stages.size(), 3U); + ASSERT_EQ(stages.size(), 2U); ASSERT_BSONOBJ_EQ(stages[0].getDocument().toBson(), fromjson("{$match: {$or: [ {'control.max.b': {$_internalExprGt: 1}}," "{$expr: {$ne: [ {$type: [ \"$control.min.b\" ]}," "{$type: [ \"$control.max.b\" ]} ]}} ]}}")); - ASSERT_BSONOBJ_EQ(stages[1].getDocument().toBson(), unpackBucketObj); - ASSERT_BSONOBJ_EQ(stages[2].getDocument().toBson(), - fromjson("{$match: {$and: [{b: {$gt: 1}}, {a: {$not: {$eq: 5}}}]}}")); + ASSERT_BSONOBJ_EQ( + stages[1].getDocument().toBson(), + fromjson( + "{$_internalUnpackBucket: {exclude: [], timeField: 'time', bucketMaxSpanSeconds: 3600, " + "eventFilter: { $and: [ { b: { $gt: 1 } }, { a: { $not: { $eq: 5 } } } ] }}}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -566,10 +587,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ASSERT_EQ(pipeline->getSources().size(), 2U); pipeline->optimizePipeline(); - ASSERT_EQ(pipeline->getSources().size(), 3U); + ASSERT_EQ(pipeline->getSources().size(), 2U); auto stages = pipeline->serializeToBson(); - ASSERT_EQ(stages.size(), 3U); + ASSERT_EQ(stages.size(), 2U); ASSERT_BSONOBJ_EQ( stages[0], @@ -582,8 +603,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, "{$or: [ {'control.min.a': {$_internalExprLt: 5}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}")); - ASSERT_BSONOBJ_EQ(stages[1], unpackBucketObj); - ASSERT_BSONOBJ_EQ(stages[2], matchObj); + ASSERT_BSONOBJ_EQ(stages[1], + fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "bucketMaxSpanSeconds: 3600," + "eventFilter: { $and: [ { b: { $gte: 2 } }, { c: { $gt: 1 } }, { a: " + "{ $lt: 5 } } ] } } }")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -601,7 +625,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate == nullptr); + ASSERT(predicate.loosePredicate == nullptr); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -619,7 +644,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate == nullptr); + ASSERT(predicate.loosePredicate == nullptr); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -637,7 +663,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT(predicate == nullptr); + ASSERT(predicate.loosePredicate == nullptr); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -656,7 +683,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ->createPredicatesOnBucketLevelField(original->getMatchExpression()); // Meta predicates are mapped to the meta field, not the control min/max fields. - ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{meta: {$gt: 5}}")); + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{meta: {$gt: 5}}")); + ASSERT_BSONOBJ_EQ(predicate.tightPredicate->serialize(true), fromjson("{meta: {$gt: 5}}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -675,7 +703,10 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, ->createPredicatesOnBucketLevelField(original->getMatchExpression()); // Meta predicates are mapped to the meta field, not the control min/max fields. - ASSERT_BSONOBJ_EQ(predicate->serialize(true), fromjson("{'meta.foo': {$gt: 5}}")); + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), + fromjson("{'meta.foo': {$gt: 5}}")); + ASSERT_BSONOBJ_EQ(predicate.tightPredicate->serialize(true), + fromjson("{'meta.foo': {$gt: 5}}")); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, @@ -693,7 +724,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$and: [" " {$or: [" " {'control.max.a': {$_internalExprGt: 1}}," @@ -704,6 +735,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, " ]}," " {meta: {$eq: 5}}" "]}")); + ASSERT(predicate.tightPredicate == nullptr); } TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePredicatesOnId) { @@ -740,7 +772,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); @@ -797,7 +829,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); @@ -846,7 +878,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 6); @@ -908,7 +940,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); @@ -957,7 +989,7 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, OptimizeMapsTimePre auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.get()); + auto andExpr = dynamic_cast<AndMatchExpression*>(predicate.loosePredicate.get()); auto children = andExpr->getChildVector(); ASSERT_EQ(children->size(), 3); @@ -1000,7 +1032,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate); + ASSERT_FALSE(predicate.loosePredicate); + ASSERT_FALSE(predicate.tightPredicate); } } { @@ -1021,7 +1054,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate); + ASSERT_FALSE(predicate.loosePredicate); + ASSERT_FALSE(predicate.tightPredicate); } } { @@ -1042,7 +1076,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate); + ASSERT_FALSE(predicate.loosePredicate); + ASSERT_FALSE(predicate.tightPredicate); } } { @@ -1065,7 +1100,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate); + ASSERT_FALSE(predicate.loosePredicate); + ASSERT_FALSE(predicate.tightPredicate); } } { @@ -1086,7 +1122,8 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_FALSE(predicate); + ASSERT_FALSE(predicate.loosePredicate); + ASSERT_FALSE(predicate.tightPredicate); } } } @@ -1107,10 +1144,11 @@ TEST_F(InternalUnpackBucketPredicateMappingOptimizationTest, auto predicate = dynamic_cast<DocumentSourceInternalUnpackBucket*>(container.front().get()) ->createPredicatesOnBucketLevelField(original->getMatchExpression()); - ASSERT_BSONOBJ_EQ(predicate->serialize(true), + ASSERT_BSONOBJ_EQ(predicate.loosePredicate->serialize(true), fromjson("{$_internalBucketGeoWithin: { withinRegion: { $geometry: { type : " "\"Polygon\" ,coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 " "] ] ]}},field: \"loc\"}}")); + ASSERT_FALSE(predicate.tightPredicate); } } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp index 70b524c7510..78b2931cdbc 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/group_reorder_test.cpp @@ -94,6 +94,49 @@ TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetadata) { ASSERT_BSONOBJ_EQ(optimized, serialized[0]); } +// Test SERVER-73822 fix: complex $min and $max (i.e. not just straight field refs) work correctly. +TEST_F(InternalUnpackBucketGroupReorder, MinMaxComplexGroupOnMetadata) { + auto unpackSpecObj = fromjson( + "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], metaField: 'meta1', timeField: 't', " + "bucketMaxSpanSeconds: 3600}}"); + auto groupSpecObj = fromjson( + "{$group: {_id: '$meta1.a.b', accmin: {$min: {$add: ['$b', {$const: 0}]}}, accmax: {$max: " + "{$add: [{$const: 0}, '$c']}}}}"); + + auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, groupSpecObj), getExpCtx()); + pipeline->optimizePipeline(); + + auto serialized = pipeline->serializeToBson(); + ASSERT_EQ(2, serialized.size()); + + // Order of fields may be different from the original stage. + auto unpackSpecResultObj = fromjson( + "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], timeField: 't', metaField: 'meta1', " + "bucketMaxSpanSeconds: 3600}}"); + ASSERT_BSONOBJ_EQ(unpackSpecResultObj, serialized[0]); + + auto groupSpecOutputObj = fromjson( + "{$group: {_id: '$meta1.a.b', accmin: {$min: {$add: ['$b', {$const: 0}]}}, accmax: {$max: " + "{$add: ['$c', {$const: 0}]}}}}"); + ASSERT_BSONOBJ_EQ(groupSpecOutputObj, serialized[1]); +} + +TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetafield) { + auto unpackSpecObj = fromjson( + "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], metaField: 'meta1', timeField: 't', " + "bucketMaxSpanSeconds: 3600}}"); + auto groupSpecObj = fromjson("{$group: {_id: '$meta1.a.b', accmin: {$min: '$meta1.f1'}}}"); + + auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, groupSpecObj), getExpCtx()); + pipeline->optimizePipeline(); + + auto serialized = pipeline->serializeToBson(); + ASSERT_EQ(1, serialized.size()); + + auto optimized = fromjson("{$group: {_id: '$meta.a.b', accmin: {$min: '$meta.f1'}}}"); + ASSERT_BSONOBJ_EQ(optimized, serialized[0]); +} + TEST_F(InternalUnpackBucketGroupReorder, MinMaxGroupOnMetadataNegative) { auto unpackSpecObj = fromjson( "{$_internalUnpackBucket: { include: ['a', 'b', 'c'], timeField: 't', metaField: 'meta', " diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp index cfe92c09f5f..0f9a60dbf2a 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/optimize_pipeline_test.cpp @@ -54,7 +54,7 @@ TEST_F(OptimizePipeline, MixedMatchPushedDown) { // To get the optimized $match from the pipeline, we have to serialize with explain. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(3u, stages.size()); + ASSERT_EQ(2u, stages.size()); // We should push down the $match on the metaField and the predicates on the control field. // The created $match stages should be added before $_internalUnpackBucket and merged. @@ -63,8 +63,10 @@ TEST_F(OptimizePipeline, MixedMatchPushedDown) { "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ] } ] } } ] }]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $lte: 4 } } } }"), + stages[1].getDocument().toBson()); } TEST_F(OptimizePipeline, MetaMatchPushedDown) { @@ -103,7 +105,7 @@ TEST_F(OptimizePipeline, MixedMatchOr) { pipeline->optimizePipeline(); auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(3u, stages.size()); + ASSERT_EQ(2u, stages.size()); auto expected = fromjson( "{$match: {$and: [" // Result of pushing down {x: {$lte: 1}}. @@ -123,8 +125,11 @@ TEST_F(OptimizePipeline, MixedMatchOr) { " ]}" "]}}"); ASSERT_BSONOBJ_EQ(expected, stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(match, stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"foo\", " + "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { $and: [ { x: { $lte: 1 } }, { $or: [ { " + "\"myMeta.a\": { $gt: 1 } }, { y: { $lt: 1 } } ] } ] } } }"), + stages[1].getDocument().toBson()); } TEST_F(OptimizePipeline, MixedMatchOnlyMetaMatchPushedDown) { @@ -142,11 +147,13 @@ TEST_F(OptimizePipeline, MixedMatchOnlyMetaMatchPushedDown) { // We should push down the $match on the metaField but not the predicate on '$a', which is // ineligible because of the $type. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); + ASSERT_EQ(2u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$gte: 0}}, {meta: {$lte: 5}}]}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(unpack, serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$type: [ 2 ]}}}"), serialized[2]); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $type: [ 2 ] } } } }"), + serialized[1]); } TEST_F(OptimizePipeline, MultipleMatchesPushedDown) { @@ -164,15 +171,17 @@ TEST_F(OptimizePipeline, MultipleMatchesPushedDown) { // We should push down both the $match on the metaField and the predicates on the control field. // The created $match stages should be added before $_internalUnpackBucket and merged. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(3u, stages.size()); + ASSERT_EQ(2u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [ {meta: {$gte: 0}}," "{meta: {$lte: 5}}," "{$or: [ {'control.min.a': {$_internalExprLte: 4}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ]}," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $lte: 4 } } } }"), + stages[1].getDocument().toBson()); } TEST_F(OptimizePipeline, MultipleMatchesPushedDownWithSort) { @@ -191,16 +200,18 @@ TEST_F(OptimizePipeline, MultipleMatchesPushedDownWithSort) { // We should push down both the $match on the metaField and the predicates on the control field. // The created $match stages should be added before $_internalUnpackBucket and merged. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(4u, stages.size()); + ASSERT_EQ(3u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [ { meta: { $gte: 0 } }," "{meta: { $lte: 5 } }," "{$or: [ { 'control.min.a': { $_internalExprLte: 4 } }," "{$expr: { $ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ] } ] } } ] }]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(unpack, stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$sort: {sortKey: {a: 1}}}"), stages[3].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $lte: 4 } } } }"), + stages[1].getDocument().toBson()); + ASSERT_BSONOBJ_EQ(fromjson("{$sort: {sortKey: {a: 1}}}"), stages[2].getDocument().toBson()); } TEST_F(OptimizePipeline, MetaMatchThenCountPushedDown) { @@ -261,7 +272,7 @@ TEST_F(OptimizePipeline, SortThenMixedMatchPushedDown) { // We should push down both the $sort and parts of the $match. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(4u, serialized.size()); + ASSERT_EQ(3u, serialized.size()); auto expected = fromjson( "{$match: {$and: [" " {meta: {$eq: 'abc'}}," @@ -274,8 +285,10 @@ TEST_F(OptimizePipeline, SortThenMixedMatchPushedDown) { "]}}"); ASSERT_BSONOBJ_EQ(expected, serialized[0]); ASSERT_BSONOBJ_EQ(fromjson("{$sort: {meta: -1}}"), serialized[1]); - ASSERT_BSONOBJ_EQ(unpack, serialized[2]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$gte: 5}}}"), serialized[3]); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $gte: 5 } } } }"), + serialized[2]); } TEST_F(OptimizePipeline, MetaMatchThenSortPushedDown) { @@ -331,18 +344,19 @@ TEST_F(OptimizePipeline, MixedMatchThenProjectPushedDown) { // We can push down part of the $match and use dependency analysis on the end of the pipeline. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(4u, stages.size()); + ASSERT_EQ(3u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$eq: 'abc'}}," "{$or: [ {'control.min.a': { $_internalExprLte: 4 } }," "{$expr: { $ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ] } ] } } ] } ]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['_id', 'a', 'x'], timeField: " - "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), - stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ( + fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"x\" ], timeField: " + "\"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $lte: 4 } } } }"), + stages[1].getDocument().toBson()); ASSERT_BSONOBJ_EQ(fromjson("{$project: {_id: true, x: true}}"), - stages[3].getDocument().toBson()); + stages[2].getDocument().toBson()); } @@ -379,21 +393,21 @@ TEST_F(OptimizePipeline, ProjectThenMixedMatchPushedDown) { // We should push down part of the $match and do dependency analysis on the rest. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(4u, stages.size()); + ASSERT_EQ(3u, stages.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{meta: {$eq: \"abc\"}}," "{$or: [ {'control.min.a': {$_internalExprLte: 4}}," "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"), stages[0].getDocument().toBson()); ASSERT_BSONOBJ_EQ( - fromjson("{$_internalUnpackBucket: { include: ['_id', 'a', 'x', 'myMeta'], timeField: " - "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), + fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"x\", \"myMeta\" ], " + "timeField: \"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $lte: 4 } } } }"), stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), stages[2].getDocument().toBson()); const UnorderedFieldsBSONObjComparator kComparator; ASSERT_EQ( kComparator.compare(fromjson("{$project: {_id: true, a: true, myMeta: true, x: true}}"), - stages[3].getDocument().toBson()), + stages[2].getDocument().toBson()), 0); } @@ -410,7 +424,7 @@ TEST_F(OptimizePipeline, ProjectWithRenameThenMixedMatchPushedDown) { // We should push down part of the $match and do dependency analysis on the end of the pipeline. auto stages = pipeline->writeExplainOps(ExplainOptions::Verbosity::kQueryPlanner); - ASSERT_EQ(4u, stages.size()); + ASSERT_EQ(3u, stages.size()); ASSERT_BSONOBJ_EQ( fromjson("{$match: {$and: [{$or: [ {'control.max.y': {$_internalExprGte: \"abc\"}}," "{$expr: {$ne: [ {$type: [ \"$control.min.y\" ]}," @@ -419,13 +433,13 @@ TEST_F(OptimizePipeline, ProjectWithRenameThenMixedMatchPushedDown) { "{$expr: {$ne: [ {$type: [ \"$control.min.a\" ] }," "{$type: [ \"$control.max.a\" ]} ]}} ]} ]}}"), stages[0].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['_id', 'a', 'y'], timeField: " - "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), - stages[1].getDocument().toBson()); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [{y: {$gte: 'abc'}}, {a: {$lte: 4}}]}}"), - stages[2].getDocument().toBson()); + ASSERT_BSONOBJ_EQ( + fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"a\", \"y\" ], timeField: " + "\"time\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { $and: [ { y: { $gte: \"abc\" } }, { a: { $lte: 4 } } ] } } }"), + stages[1].getDocument().toBson()); ASSERT_BSONOBJ_EQ(fromjson("{$project: {_id: true, a: true, myMeta: '$y'}}"), - stages[3].getDocument().toBson()); + stages[2].getDocument().toBson()); } TEST_F(OptimizePipeline, ComputedProjectThenMetaMatchPushedDown) { @@ -466,15 +480,15 @@ TEST_F(OptimizePipeline, ComputedProjectThenMetaMatchNotPushedDown) { // We should both push down the project and internalize the remaining project, but we can't // push down the meta match due to the (now invalid) renaming. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); + ASSERT_EQ(2u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {myMeta: {$sum: ['$meta.a', '$meta.b']}}}"), serialized[0]); ASSERT_BSONOBJ_EQ( - fromjson( - "{$_internalUnpackBucket: { include: ['_id', 'myMeta'], timeField: 'time', metaField: " - "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['myMeta']}}"), + fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"myMeta\" ], timeField: " + "\"time\", metaField: \"myMeta\", " + "bucketMaxSpanSeconds: 3600, computedMetaProjFields: [ \"myMeta\" ], " + "eventFilter: { myMeta: { $gte: \"abc\" } } } }"), serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {myMeta: {$gte: 'abc'}}}"), serialized[2]); } // namespace TEST_F(OptimizePipeline, ComputedProjectThenMatchNotPushedDown) { @@ -491,13 +505,13 @@ TEST_F(OptimizePipeline, ComputedProjectThenMatchNotPushedDown) { // We should push down the computed project but not the match, because it depends on the newly // computed values. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); + ASSERT_EQ(2u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {y: {$sum: ['$meta.a', '$meta.b']}}}"), serialized[0]); - ASSERT_BSONOBJ_EQ( - fromjson("{$_internalUnpackBucket: { include: ['_id', 'y'], timeField: 'time', metaField: " - "'myMeta', bucketMaxSpanSeconds: 3600, computedMetaProjFields: ['y']}}"), - serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {y: {$gt: 'abc'}}}"), serialized[2]); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"y\" ], " + "timeField: \"time\", metaField: \"myMeta\", " + "bucketMaxSpanSeconds: 3600, computedMetaProjFields: [ \"y\" ], " + "eventFilter: { y: { $gt: \"abc\" } } } }"), + serialized[1]); } TEST_F(OptimizePipeline, MetaSortThenProjectPushedDown) { @@ -750,6 +764,55 @@ TEST_F(OptimizePipeline, InternalizeProjectAndPushdownAddFields) { serialized[1]); } +TEST_F(OptimizePipeline, DoNotSwapAddFieldsIfDependencyIsExcluded) { + { + auto unpackSpecObj = fromjson( + "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " + "bucketMaxSpanSeconds: 3600}}"); + auto projectSpecObj = fromjson("{$project: {x: true, _id: false}}"); + auto addFieldsSpec = fromjson("{$addFields: {newMeta: '$myMeta'}}"); + + auto pipeline = + Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj, addFieldsSpec), getExpCtx()); + + pipeline->optimizePipeline(); + + // We should internalize the $project but _not_ push down the $addFields because it's field + // dependency has been excluded. Theoretically we could remove the $addFields for this + // trivial except but not always. + auto serialized = pipeline->serializeToBson(); + ASSERT_EQ(2u, serialized.size()); + ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['x'], timeField: 'time', " + "metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), + serialized[0]); + ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {newMeta: '$myMeta'}}"), serialized[1]); + } + + // Similar test except the dependency is on an excluded non-meta field. + { + auto unpackSpecObj = fromjson( + "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " + "bucketMaxSpanSeconds: 3600}}"); + auto projectSpecObj = fromjson("{$project: {x: true, _id: false}}"); + auto addFieldsSpec = fromjson("{$addFields: {newMeta: '$excluded'}}"); + + auto pipeline = + Pipeline::parse(makeVector(unpackSpecObj, projectSpecObj, addFieldsSpec), getExpCtx()); + + pipeline->optimizePipeline(); + + // We should internalize the $project but _not_ push down the $addFields because it's field + // dependency has been excluded. Theoretically we could remove the $addFields for this + // trivial except but not always. + auto serialized = pipeline->serializeToBson(); + ASSERT_EQ(2u, serialized.size()); + ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: { include: ['x'], timeField: 'time', " + "metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), + serialized[0]); + ASSERT_BSONOBJ_EQ(fromjson("{$addFields: {newMeta: '$excluded'}}"), serialized[1]); + } +} + TEST_F(OptimizePipeline, PushdownSortAndAddFields) { auto unpackSpecObj = fromjson( "{$_internalUnpackBucket: { exclude: [], timeField: 'time', metaField: 'myMeta', " @@ -808,7 +871,7 @@ TEST_F(OptimizePipeline, MatchWithGeoWithinOnMeasurementsPushedDownUsingInternal pipeline->optimizePipeline(); auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(serialized.size(), 3U); + ASSERT_EQ(serialized.size(), 2U); // $match with $geoWithin on a non-metadata field is pushed down and $_internalBucketGeoWithin // is used. @@ -817,12 +880,12 @@ TEST_F(OptimizePipeline, MatchWithGeoWithinOnMeasurementsPushedDownUsingInternal "\"Polygon\" ,coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 " "] ] ]}},field: \"loc\"}}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: {exclude: [], timeField: " - "'time', bucketMaxSpanSeconds: 3600}}"), - serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {loc: {$geoWithin: {$geometry: {type: \"Polygon\", " - "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]}}}}}"), - serialized[2]); + ASSERT_BSONOBJ_EQ( + fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "bucketMaxSpanSeconds: 3600, " + "eventFilter: { loc: { $geoWithin: { $geometry: { type: \"Polygon\", coordinates: " + "[ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ] } } } } } }"), + serialized[1]); } TEST_F(OptimizePipeline, MatchWithGeoWithinOnMetaFieldIsPushedDown) { @@ -864,7 +927,7 @@ TEST_F(OptimizePipeline, pipeline->optimizePipeline(); auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(serialized.size(), 3U); + ASSERT_EQ(serialized.size(), 2U); // $match with $geoIntersects on a non-metadata field is pushed down and // $_internalBucketGeoWithin is used. @@ -873,12 +936,12 @@ TEST_F(OptimizePipeline, "\"Polygon\" ,coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 " "] ] ]}},field: \"loc\"}}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{$_internalUnpackBucket: {exclude: [], timeField: " - "'time', bucketMaxSpanSeconds: 3600}}"), - serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {loc: {$geoIntersects: {$geometry: {type: \"Polygon\", " - "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ]}}}}}"), - serialized[2]); + ASSERT_BSONOBJ_EQ( + fromjson("{ $_internalUnpackBucket: { exclude: [], timeField: \"time\", " + "bucketMaxSpanSeconds: 3600, " + "eventFilter: { loc: { $geoIntersects: { $geometry: { type: \"Polygon\", " + "coordinates: [ [ [ 0, 0 ], [ 3, 6 ], [ 6, 1 ], [ 0, 0 ] ] ] } } } } } }"), + serialized[1]); } TEST_F(OptimizePipeline, MatchWithGeoIntersectsOnMetaFieldIsPushedDown) { @@ -907,5 +970,56 @@ TEST_F(OptimizePipeline, MatchWithGeoIntersectsOnMetaFieldIsPushedDown) { "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"), serialized[1]); } + +TEST_F(OptimizePipeline, StreamingGroupIsEnabledWhenPossible) { + auto unpackSpecObj = fromjson( + "{$_internalUnpackBucket: {exclude: [], timeField: " + "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"); + auto groupSpecObj = fromjson( + "{$group: {_id: {hour: {$dateTrunc: {date: '$time', unit: 'hour'}}, symbol: " + "'$myMeta.symbol'}" + ", 'sum': {$sum: '$tradeAmount'}}}"); + auto pipeline = Pipeline::parse(makeVector(unpackSpecObj, + fromjson("{$sort: {time: 1}}"), + fromjson("{$match: {'tradePrice': 100}}"), + groupSpecObj), + getExpCtx()); + + ASSERT_EQ(pipeline->getSources().size(), 4U); + + pipeline->optimizePipeline(); + + auto serialized = pipeline->serializeToBson(); + ASSERT_EQ(serialized.size(), 4U); + + auto streamingGroupSpecObj = fromjson( + "{$_internalStreamingGroup: {_id: {hour: {$dateTrunc: {date: '$time', unit: {$const: " + "'hour'}}}, symbol: '$myMeta.symbol'}, 'sum': {$sum: '$tradeAmount'}, " + "'$monotonicIdFields': ['hour']}}"); + ASSERT_BSONOBJ_EQ(streamingGroupSpecObj, serialized.back()); +} + +TEST_F(OptimizePipeline, StreamingGroupIsNotEnabledWhenTimeFieldIsModified) { + auto unpackSpecObj = fromjson( + "{$_internalUnpackBucket: {exclude: [], timeField: " + "'time', metaField: 'myMeta', bucketMaxSpanSeconds: 3600}}"); + auto groupSpecObj = fromjson( + "{$group: {_id: {hour: '$time', symbol: '$myMeta.symbol'}, 'sum': {$sum: " + "'$tradeAmount'}}}"); + auto pipeline = Pipeline::parse( + makeVector(unpackSpecObj, + fromjson("{$addFields: {'time': {$dateTrunc: {date: '$time', unit: 'hour'}}}}"), + fromjson("{$sort: {time: 1}}"), + groupSpecObj), + getExpCtx()); + + ASSERT_EQ(pipeline->getSources().size(), 4U); + + pipeline->optimizePipeline(); + + auto serialized = pipeline->serializeToBson(); + ASSERT_EQ(serialized.size(), 4U); + ASSERT_BSONOBJ_EQ(groupSpecObj, serialized.back()); +} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp index ba4f31adf17..4ce5d558ac4 100644 --- a/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp +++ b/src/mongo/db/pipeline/document_source_internal_unpack_bucket_test/split_match_on_meta_and_rename_test.cpp @@ -56,7 +56,7 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeSplitsMatchAndMaps // predicate on 'control.min.a'. These two created $match stages should be added before // $_internalUnpackBucket and merged. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); + ASSERT_EQ(2u, serialized.size()); ASSERT_BSONOBJ_EQ(fromjson("{$match: {$and: [" " {meta: {$gte: 0}}," " {meta: {$lte: 5}}," @@ -68,8 +68,13 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeSplitsMatchAndMaps " ]}" "]}}"), serialized[0]); - ASSERT_BSONOBJ_EQ(unpack, serialized[1]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {a: {$lte: 4}}}"), serialized[2]); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { " + "exclude: [], " + "timeField: \"foo\", " + "metaField: \"myMeta\", " + "bucketMaxSpanSeconds: 3600, " + "eventFilter: { a: { $lte: 4 } } } }"), + serialized[1]); } TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, OptimizeMovesMetaMatchBeforeUnpack) { @@ -94,10 +99,6 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, auto unpack = fromjson( "{$_internalUnpackBucket: { exclude: [], timeField: 'foo', metaField: 'myMeta', " "bucketMaxSpanSeconds: 3600}}"); - auto unpackExcluded = fromjson( - "{$_internalUnpackBucket: { include: ['_id', 'data'], timeField: 'foo', metaField: " - "'myMeta', " - "bucketMaxSpanSeconds: 3600}}"); auto pipeline = Pipeline::parse(makeVector(unpack, fromjson("{$project: {data: 1}}"), fromjson("{$match: {myMeta: {$gte: 0}}}")), @@ -108,9 +109,11 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, // The $match on meta is not moved before $_internalUnpackBucket since the field is excluded. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(2u, serialized.size()); - ASSERT_BSONOBJ_EQ(unpackExcluded, serialized[0]); - ASSERT_BSONOBJ_EQ(fromjson("{$match: {myMeta: {$gte: 0}}}"), serialized[1]); + ASSERT_EQ(1u, serialized.size()); + ASSERT_BSONOBJ_EQ(fromjson("{ $_internalUnpackBucket: { include: [ \"_id\", \"data\" ], " + "timeField: \"foo\", metaField: \"myMeta\", bucketMaxSpanSeconds: " + "3600, eventFilter: { myMeta: { $gte: 0 } } } }"), + serialized[0]); } TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, @@ -134,7 +137,7 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, // We should fail to split the match because of the $or clause. We should still be able to // map the predicate on 'x' to a predicate on the control field. auto serialized = pipeline->serializeToBson(); - ASSERT_EQ(3u, serialized.size()); + ASSERT_EQ(2u, serialized.size()); auto expected = fromjson( "{$match: {$and: [" // Result of pushing down {x: {$lte: 1}}. @@ -154,8 +157,13 @@ TEST_F(InternalUnpackBucketSplitMatchOnMetaAndRename, " ]}" "]}}"); ASSERT_BSONOBJ_EQ(expected, serialized[0]); - ASSERT_BSONOBJ_EQ(unpack, serialized[1]); - ASSERT_BSONOBJ_EQ(match, serialized[2]); + ASSERT_BSONOBJ_EQ( + fromjson( + "{ $_internalUnpackBucket: { " + "exclude: [], timeField: \"foo\", metaField: \"myMeta\", bucketMaxSpanSeconds: 3600, " + "eventFilter: { $and: [ { x: { $lte: 1 } }, { $or: [ { \"myMeta.a\": { $gt: 1 } }, { " + "y: { $lt: 1 } } ] } ] } } }"), + serialized[1]); } } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_lookup.cpp b/src/mongo/db/pipeline/document_source_lookup.cpp index cfb885e94c5..0b4585de2bc 100644 --- a/src/mongo/db/pipeline/document_source_lookup.cpp +++ b/src/mongo/db/pipeline/document_source_lookup.cpp @@ -35,6 +35,7 @@ #include "mongo/db/exec/document_value/value.h" #include "mongo/db/jsobj.h" #include "mongo/db/matcher/expression_algo.h" +#include "mongo/db/namespace_string.h" #include "mongo/db/pipeline/aggregation_request_helper.h" #include "mongo/db/pipeline/document_path_support.h" #include "mongo/db/pipeline/document_source_documents.h" @@ -108,7 +109,8 @@ NamespaceString parseLookupFromAndResolveNamespace(const BSONElement& elem, Stri str::stream() << "$lookup with syntax {from: {db:<>, coll:<>},..} is not supported for db: " << nss.db() << " and coll: " << nss.coll(), nss.isConfigDotCacheDotChunks() || nss == NamespaceString::kRsOplogNamespace || - nss == NamespaceString::kTenantMigrationOplogView); + nss == NamespaceString::kTenantMigrationOplogView || + nss == NamespaceString::kConfigsvrCollectionsNamespace); return nss; } @@ -399,6 +401,11 @@ StageConstraints DocumentSourceLookUp::constraints(Pipeline::SplitState pipeStat // This stage will only be on the shards pipeline if $lookup on sharded foreign collections // is allowed. hostRequirement = HostTypeRequirement::kAnyShard; + } else if (_fromNs == NamespaceString::kConfigsvrCollectionsNamespace) { + // This is an unsharded collection, but the primary shard would be the config server, and + // the config servers are not prepared to take queries. Instead, we'll merge on any of the + // other shards. + hostRequirement = HostTypeRequirement::kAnyShard; } else { // If the pipeline is unsplit or this stage is on the merging part of the pipeline, // when $lookup on sharded foreign collections is allowed, the foreign collection is @@ -661,7 +668,7 @@ void DocumentSourceLookUp::addCacheStageAndOptimize(Pipeline& pipeline) { } DocumentSource::GetModPathsReturn DocumentSourceLookUp::getModifiedPaths() const { - std::set<std::string> modifiedPaths{_as.fullPath()}; + OrderedPathSet modifiedPaths{_as.fullPath()}; if (_unwindSrc) { auto pathsModifiedByUnwind = _unwindSrc->getModifiedPaths(); invariant(pathsModifiedByUnwind.type == GetModPathsReturn::Type::kFiniteSet); diff --git a/src/mongo/db/pipeline/document_source_match.cpp b/src/mongo/db/pipeline/document_source_match.cpp index 10346eb6a46..418f1821b4b 100644 --- a/src/mongo/db/pipeline/document_source_match.cpp +++ b/src/mongo/db/pipeline/document_source_match.cpp @@ -93,11 +93,12 @@ DocumentSource::GetNextResult DocumentSourceMatch::doGetNext() { auto nextInput = pSource->getNext(); for (; nextInput.isAdvanced(); nextInput = pSource->getNext()) { // MatchExpression only takes BSON documents, so we have to make one. As an optimization, - // only serialize the fields we need to do the match. + // only serialize the fields we need to do the match. Specify BSONObj::LargeSizeTrait so + // that matching against a large document mid-pipeline does not throw a BSON max-size error. BSONObj toMatch = _dependencies.needWholeDocument - ? nextInput.getDocument().toBson() - : document_path_support::documentToBsonWithPaths(nextInput.getDocument(), - _dependencies.fields); + ? nextInput.getDocument().toBson<BSONObj::LargeSizeTrait>() + : document_path_support::documentToBsonWithPaths<BSONObj::LargeSizeTrait>( + nextInput.getDocument(), _dependencies.fields); if (_expression->matchesBSON(toMatch)) { return nextInput; @@ -410,13 +411,13 @@ void DocumentSourceMatch::joinMatchWith(intrusive_ptr<DocumentSourceMatch> other } pair<intrusive_ptr<DocumentSourceMatch>, intrusive_ptr<DocumentSourceMatch>> -DocumentSourceMatch::splitSourceBy(const std::set<std::string>& fields, +DocumentSourceMatch::splitSourceBy(const OrderedPathSet& fields, const StringMap<std::string>& renames) && { return std::move(*this).splitSourceByFunc(fields, renames, expression::isIndependentOf); } pair<intrusive_ptr<DocumentSourceMatch>, intrusive_ptr<DocumentSourceMatch>> -DocumentSourceMatch::splitSourceByFunc(const std::set<std::string>& fields, +DocumentSourceMatch::splitSourceByFunc(const OrderedPathSet& fields, const StringMap<std::string>& renames, expression::ShouldSplitExprFunc func) && { pair<unique_ptr<MatchExpression>, unique_ptr<MatchExpression>> newExpr( @@ -499,7 +500,7 @@ DocumentSourceMatch::splitMatchByModifiedFields( const boost::intrusive_ptr<DocumentSourceMatch>& match, const DocumentSource::GetModPathsReturn& modifiedPathsRet) { // Attempt to move some or all of this $match before this stage. - std::set<std::string> modifiedPaths; + OrderedPathSet modifiedPaths; switch (modifiedPathsRet.type) { case DocumentSource::GetModPathsReturn::Type::kNotSupported: // We don't know what paths this stage might modify, so refrain from swapping. diff --git a/src/mongo/db/pipeline/document_source_match.h b/src/mongo/db/pipeline/document_source_match.h index 5bf27ddfe5c..f01c655771d 100644 --- a/src/mongo/db/pipeline/document_source_match.h +++ b/src/mongo/db/pipeline/document_source_match.h @@ -140,7 +140,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // This stage does not modify or rename any paths. - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } /** @@ -199,7 +199,7 @@ public: * z: "baz"}} and {$match: {a: "foo"}}. */ std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>> - splitSourceBy(const std::set<std::string>& fields, const StringMap<std::string>& renames) &&; + splitSourceBy(const OrderedPathSet& fields, const StringMap<std::string>& renames) &&; boost::optional<DistributedPlanLogic> distributedPlanLogic() final { return boost::none; @@ -220,7 +220,7 @@ protected: private: std::pair<boost::intrusive_ptr<DocumentSourceMatch>, boost::intrusive_ptr<DocumentSourceMatch>> - splitSourceByFunc(const std::set<std::string>& fields, + splitSourceByFunc(const OrderedPathSet& fields, const StringMap<std::string>& renames, expression::ShouldSplitExprFunc func) &&; diff --git a/src/mongo/db/pipeline/document_source_merge.cpp b/src/mongo/db/pipeline/document_source_merge.cpp index 96a1dd55547..e2746296f01 100644 --- a/src/mongo/db/pipeline/document_source_merge.cpp +++ b/src/mongo/db/pipeline/document_source_merge.cpp @@ -57,10 +57,11 @@ namespace { using MergeStrategyDescriptor = DocumentSourceMerge::MergeStrategyDescriptor; using MergeMode = MergeStrategyDescriptor::MergeMode; using MergeStrategy = MergeStrategyDescriptor::MergeStrategy; +using BatchedCommandGenerator = MergeStrategyDescriptor::BatchedCommandGenerator; using MergeStrategyDescriptorsMap = std::map<const MergeMode, const MergeStrategyDescriptor>; using WhenMatched = MergeStrategyDescriptor::WhenMatched; using WhenNotMatched = MergeStrategyDescriptor::WhenNotMatched; -using BatchTransform = std::function<void(DocumentSourceMerge::BatchedObjects&)>; +using BatchTransform = DocumentSourceMerge::BatchTransform; using UpdateModification = write_ops::UpdateModification; using UpsertType = MongoProcessInterface::UpsertType; @@ -83,21 +84,71 @@ constexpr auto kPipelineDiscardMode = MergeMode{WhenMatched::kPipeline, WhenNotM const auto kDefaultPipelineLet = BSON("new" << "$$ROOT"); +BatchedCommandGenerator makeInsertCommandGenerator() { + return [](const auto& expCtx, const auto& ns) -> BatchedCommandRequest { + return DocumentSourceMerge::DocumentSourceWriter::makeInsertCommand( + ns, expCtx->bypassDocumentValidation); + }; +} + +BatchedCommandGenerator makeUpdateCommandGenerator() { + return [](const auto& expCtx, const auto& ns) -> BatchedCommandRequest { + write_ops::UpdateCommandRequest updateOp(ns); + updateOp.setWriteCommandRequestBase([&] { + write_ops::WriteCommandRequestBase wcb; + wcb.setOrdered(false); + wcb.setBypassDocumentValidation(expCtx->bypassDocumentValidation); + return wcb; + }()); + auto [constants, letParams] = + expCtx->variablesParseState.transitionalCompatibilitySerialize(expCtx->variables); + updateOp.setLegacyRuntimeConstants(std::move(constants)); + if (!letParams.isEmpty()) { + updateOp.setLet(std::move(letParams)); + } + return BatchedCommandRequest(std::move(updateOp)); + }; +} + /** - * Creates a merge strategy which uses update semantics to perform a merge operation. If - * 'BatchTransform' function is provided, it will be called to transform batched objects before - * passing them to the 'update'. + * Converts 'batch' into a vector of UpdateOpEntries. */ -MergeStrategy makeUpdateStrategy(UpsertType upsert, BatchTransform transform) { - return [upsert, transform]( - const auto& expCtx, const auto& ns, const auto& wc, auto epoch, auto&& batch) { - if (transform) { - transform(batch); - } +std::vector<write_ops::UpdateOpEntry> constructUpdateEntries( + DocumentSourceMerge::DocumentSourceWriter::BatchedObjects&& batch, + UpsertType upsert, + bool multi) { + std::vector<write_ops::UpdateOpEntry> updateEntries; + for (auto&& obj : batch) { + write_ops::UpdateOpEntry entry; + auto&& [q, u, c] = obj; + entry.setQ(std::move(q)); + entry.setU(std::move(u)); + entry.setC(std::move(c)); + entry.setUpsert(upsert != UpsertType::kNone); + entry.setUpsertSupplied({{entry.getUpsert(), upsert == UpsertType::kInsertSuppliedDoc}}); + entry.setMulti(multi); + + updateEntries.push_back(std::move(entry)); + } + return updateEntries; +} +/** + * Creates a merge strategy which uses update semantics to perform a merge operation. + */ +MergeStrategy makeUpdateStrategy() { + return [](const auto& expCtx, + const auto& ns, + const auto& wc, + auto epoch, + auto&& batch, + auto&& bcr, + UpsertType upsert) { constexpr auto multi = false; + auto updateCommand = bcr.extractUpdateRequest(); + updateCommand->setUpdates(constructUpdateEntries(std::move(batch), upsert, multi)); uassertStatusOK(expCtx->mongoProcessInterface->update( - expCtx, ns, std::move(batch), wc, upsert, multi, epoch)); + expCtx, ns, std::move(updateCommand), wc, upsert, multi, epoch)); }; } @@ -106,20 +157,22 @@ MergeStrategy makeUpdateStrategy(UpsertType upsert, BatchTransform transform) { * that each document in the batch has a matching document in the 'ns' collection (note that a * matching document may not be modified as a result of an update operation, yet it still will be * counted as matching). If at least one document doesn't have a match, this strategy returns an - * error. If 'BatchTransform' function is provided, it will be called to transform batched objects - * before passing them to the 'update'. + * error. */ -MergeStrategy makeStrictUpdateStrategy(UpsertType upsert, BatchTransform transform) { - return [upsert, transform]( - const auto& expCtx, const auto& ns, const auto& wc, auto epoch, auto&& batch) { - if (transform) { - transform(batch); - } - +MergeStrategy makeStrictUpdateStrategy() { + return [](const auto& expCtx, + const auto& ns, + const auto& wc, + auto epoch, + auto&& batch, + auto&& bcr, + UpsertType upsert) { const int64_t batchSize = batch.size(); constexpr auto multi = false; + auto updateCommand = bcr.extractUpdateRequest(); + updateCommand->setUpdates(constructUpdateEntries(std::move(batch), upsert, multi)); auto updateResult = uassertStatusOK(expCtx->mongoProcessInterface->update( - expCtx, ns, std::move(batch), wc, upsert, multi, epoch)); + expCtx, ns, std::move(updateCommand), wc, upsert, multi, epoch)); uassert(ErrorCodes::MergeStageNoMatchingDocument, "{} could not find a matching document in the target collection " "for at least one document in the source collection"_format(kStageName), @@ -131,28 +184,34 @@ MergeStrategy makeStrictUpdateStrategy(UpsertType upsert, BatchTransform transfo * Creates a merge strategy which uses insert semantics to perform a merge operation. */ MergeStrategy makeInsertStrategy() { - return [](const auto& expCtx, const auto& ns, const auto& wc, auto epoch, auto&& batch) { + return [](const auto& expCtx, + const auto& ns, + const auto& wc, + auto epoch, + auto&& batch, + auto&& bcr, + UpsertType upsertType) { std::vector<BSONObj> objectsToInsert(batch.size()); // The batch stores replacement style updates, but for this "insert" style of $merge we'd // like to just insert the new document without attempting any sort of replacement. std::transform(batch.begin(), batch.end(), objectsToInsert.begin(), [](const auto& obj) { return std::get<UpdateModification>(obj).getUpdateReplacement(); }); - uassertStatusOK(expCtx->mongoProcessInterface->insert( - expCtx, ns, std::move(objectsToInsert), wc, epoch)); + auto insertCommand = bcr.extractInsertRequest(); + insertCommand->setDocuments(std::move(objectsToInsert)); + uassertStatusOK( + expCtx->mongoProcessInterface->insert(expCtx, ns, std::move(insertCommand), wc, epoch)); }; } /** - * Creates a batched objects transformation function which wraps each element of the - * 'batch.modifications' array into the given 'updateOp' operator. + * Creates a batched object transformation function which wraps 'obj' into the given 'updateOp' + * operator. */ BatchTransform makeUpdateTransform(const std::string& updateOp) { - return [updateOp](auto& batch) { - for (auto&& obj : batch) { - std::get<UpdateModification>(obj) = UpdateModification::parseFromClassicUpdate( - BSON(updateOp << std::get<UpdateModification>(obj).getUpdateReplacement())); - } + return [updateOp](auto& obj) { + std::get<UpdateModification>(obj) = UpdateModification::parseFromClassicUpdate( + BSON(updateOp << std::get<UpdateModification>(obj).getUpdateReplacement())); }; } @@ -171,53 +230,95 @@ const MergeStrategyDescriptorsMap& getDescriptors() { // be initialized first. By wrapping the map into a function we can guarantee that it won't be // initialized until the first use, which is when the program already started and all global // variables had been initialized. - static const auto mergeStrategyDescriptors = MergeStrategyDescriptorsMap{ - // whenMatched: replace, whenNotMatched: insert - {kReplaceInsertMode, - {kReplaceInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(UpsertType::kGenerateNewDoc, {})}}, - // whenMatched: replace, whenNotMatched: fail - {kReplaceFailMode, - {kReplaceFailMode, {ActionType::update}, makeStrictUpdateStrategy(UpsertType::kNone, {})}}, - // whenMatched: replace, whenNotMatched: discard - {kReplaceDiscardMode, - {kReplaceDiscardMode, {ActionType::update}, makeUpdateStrategy(UpsertType::kNone, {})}}, - // whenMatched: merge, whenNotMatched: insert - {kMergeInsertMode, - {kMergeInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(UpsertType::kGenerateNewDoc, makeUpdateTransform("$set"))}}, - // whenMatched: merge, whenNotMatched: fail - {kMergeFailMode, - {kMergeFailMode, - {ActionType::update}, - makeStrictUpdateStrategy(UpsertType::kNone, makeUpdateTransform("$set"))}}, - // whenMatched: merge, whenNotMatched: discard - {kMergeDiscardMode, - {kMergeDiscardMode, - {ActionType::update}, - makeUpdateStrategy(UpsertType::kNone, makeUpdateTransform("$set"))}}, - // whenMatched: keepExisting, whenNotMatched: insert - {kKeepExistingInsertMode, - {kKeepExistingInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(UpsertType::kGenerateNewDoc, makeUpdateTransform("$setOnInsert"))}}, - // whenMatched: [pipeline], whenNotMatched: insert - {kPipelineInsertMode, - {kPipelineInsertMode, - {ActionType::insert, ActionType::update}, - makeUpdateStrategy(UpsertType::kInsertSuppliedDoc, {})}}, - // whenMatched: [pipeline], whenNotMatched: fail - {kPipelineFailMode, - {kPipelineFailMode, - {ActionType::update}, - makeStrictUpdateStrategy(UpsertType::kNone, {})}}, - // whenMatched: [pipeline], whenNotMatched: discard - {kPipelineDiscardMode, - {kPipelineDiscardMode, {ActionType::update}, makeUpdateStrategy(UpsertType::kNone, {})}}, - // whenMatched: fail, whenNotMatched: insert - {kFailInsertMode, {kFailInsertMode, {ActionType::insert}, makeInsertStrategy()}}}; + static const auto mergeStrategyDescriptors = + MergeStrategyDescriptorsMap{// whenMatched: replace, whenNotMatched: insert + {kReplaceInsertMode, + {kReplaceInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(), + {}, + UpsertType::kGenerateNewDoc, + makeUpdateCommandGenerator()}}, + // whenMatched: replace, whenNotMatched: fail + {kReplaceFailMode, + {kReplaceFailMode, + {ActionType::update}, + makeStrictUpdateStrategy(), + {}, + UpsertType::kNone, + makeUpdateCommandGenerator()}}, + // whenMatched: replace, whenNotMatched: discard + {kReplaceDiscardMode, + {kReplaceDiscardMode, + {ActionType::update}, + makeUpdateStrategy(), + {}, + UpsertType::kNone, + makeUpdateCommandGenerator()}}, + // whenMatched: merge, whenNotMatched: insert + {kMergeInsertMode, + {kMergeInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(), + makeUpdateTransform("$set"), + UpsertType::kGenerateNewDoc, + makeUpdateCommandGenerator()}}, + // whenMatched: merge, whenNotMatched: fail + {kMergeFailMode, + {kMergeFailMode, + {ActionType::update}, + makeStrictUpdateStrategy(), + makeUpdateTransform("$set"), + UpsertType::kNone, + makeUpdateCommandGenerator()}}, + // whenMatched: merge, whenNotMatched: discard + {kMergeDiscardMode, + {kMergeDiscardMode, + {ActionType::update}, + makeUpdateStrategy(), + makeUpdateTransform("$set"), + UpsertType::kNone, + makeUpdateCommandGenerator()}}, + // whenMatched: keepExisting, whenNotMatched: insert + {kKeepExistingInsertMode, + {kKeepExistingInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(), + makeUpdateTransform("$setOnInsert"), + UpsertType::kGenerateNewDoc, + makeUpdateCommandGenerator()}}, + // whenMatched: [pipeline], whenNotMatched: insert + {kPipelineInsertMode, + {kPipelineInsertMode, + {ActionType::insert, ActionType::update}, + makeUpdateStrategy(), + {}, + UpsertType::kInsertSuppliedDoc, + makeUpdateCommandGenerator()}}, + // whenMatched: [pipeline], whenNotMatched: fail + {kPipelineFailMode, + {kPipelineFailMode, + {ActionType::update}, + makeStrictUpdateStrategy(), + {}, + UpsertType::kNone, + makeUpdateCommandGenerator()}}, + // whenMatched: [pipeline], whenNotMatched: discard + {kPipelineDiscardMode, + {kPipelineDiscardMode, + {ActionType::update}, + makeUpdateStrategy(), + {}, + UpsertType::kNone, + makeUpdateCommandGenerator()}}, + // whenMatched: fail, whenNotMatched: insert + {kFailInsertMode, + {kFailInsertMode, + {ActionType::insert}, + makeInsertStrategy(), + {}, + UpsertType::kNone, + makeInsertCommandGenerator()}}}; return mergeStrategyDescriptors; } @@ -543,17 +644,29 @@ std::pair<DocumentSourceMerge::BatchObject, int> DocumentSourceMerge::makeBatchO auto mergeOnFields = extractMergeOnFieldsFromDoc(doc, _mergeOnFields); auto mod = makeBatchUpdateModification(doc); auto vars = resolveLetVariablesIfNeeded(doc); - auto modSize = mod.objsize() + (vars ? vars->objsize() : 0); - return {{std::move(mergeOnFields), std::move(mod), std::move(vars)}, modSize}; + BatchObject batchObject{std::move(mergeOnFields), std::move(mod), std::move(vars)}; + if (_descriptor.transform) { + _descriptor.transform(batchObject); + } + + tassert(6628901, "_writeSizeEstimator should be initialized", _writeSizeEstimator); + return {batchObject, + _writeSizeEstimator->estimateUpdateSizeBytes(batchObject, _descriptor.upsertType)}; } -void DocumentSourceMerge::spill(BatchedObjects&& batch) try { +void DocumentSourceMerge::spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) try { DocumentSourceWriteBlock writeBlock(pExpCtx->opCtx); auto targetEpoch = _targetCollectionVersion ? boost::optional<OID>(_targetCollectionVersion->epoch()) : boost::none; - _descriptor.strategy(pExpCtx, _outputNs, _writeConcern, targetEpoch, std::move(batch)); + _descriptor.strategy(pExpCtx, + _outputNs, + _writeConcern, + targetEpoch, + std::move(batch), + std::move(bcr), + _descriptor.upsertType); } catch (const ExceptionFor<ErrorCodes::ImmutableField>& ex) { uassertStatusOKWithContext(ex.toStatus(), "$merge failed to update the matching document, did you " @@ -577,6 +690,10 @@ void DocumentSourceMerge::spill(BatchedObjects&& batch) try { } } +BatchedCommandRequest DocumentSourceMerge::initializeBatchedWriteRequest() const { + return _descriptor.batchedCommandGenerator(pExpCtx, _outputNs); +} + void DocumentSourceMerge::waitWhileFailPointEnabled() { CurOpFailpointHelpers::waitWhileFailPointEnabled( &hangWhileBuildingDocumentSourceMergeBatch, diff --git a/src/mongo/db/pipeline/document_source_merge.h b/src/mongo/db/pipeline/document_source_merge.h index 05a87f1e340..9388add24ba 100644 --- a/src/mongo/db/pipeline/document_source_merge.h +++ b/src/mongo/db/pipeline/document_source_merge.h @@ -44,24 +44,38 @@ class DocumentSourceMerge final : public DocumentSourceWriter<MongoProcessInterf public: static constexpr StringData kStageName = "$merge"_sd; - // A descriptor for a merge strategy. Holds a merge strategy function and a set of actions - // the client should be authorized to perform in order to be able to execute a merge operation - // using this merge strategy. + using BatchTransform = std::function<void(MongoProcessInterface::BatchObject&)>; + + // A descriptor for a merge strategy. Holds a merge strategy function and a set of actions the + // client should be authorized to perform in order to be able to execute a merge operation using + // this merge strategy. Additionally holds a 'BatchedCommandGenerator' that will initialize a + // BatchedWriteRequest for executing the batch write. If a 'BatchTransform' function is + // provided, it will be called when constructing a batch object to transform updates. struct MergeStrategyDescriptor { using WhenMatched = MergeWhenMatchedModeEnum; using WhenNotMatched = MergeWhenNotMatchedModeEnum; using MergeMode = std::pair<WhenMatched, WhenNotMatched>; + using UpsertType = MongoProcessInterface::UpsertType; // A function encapsulating a merge strategy for the $merge stage based on the pair of // whenMatched/whenNotMatched modes. using MergeStrategy = std::function<void(const boost::intrusive_ptr<ExpressionContext>&, const NamespaceString&, const WriteConcernOptions&, boost::optional<OID>, - BatchedObjects&&)>; + BatchedObjects&&, + BatchedCommandRequest&&, + UpsertType upsert)>; + + // A function object that will be invoked to generate a BatchedCommandRequest. + using BatchedCommandGenerator = std::function<BatchedCommandRequest( + const boost::intrusive_ptr<ExpressionContext>&, const NamespaceString&)>; MergeMode mode; ActionSet actions; MergeStrategy strategy; + BatchTransform transform; + UpsertType upsertType; + BatchedCommandGenerator batchedCommandGenerator; }; /** @@ -177,8 +191,11 @@ private: * Creates an UpdateModification object from the given 'doc' to be used with the batched update. */ auto makeBatchUpdateModification(const Document& doc) const { - return _pipeline ? write_ops::UpdateModification(*_pipeline) - : write_ops::UpdateModification::parseFromClassicUpdate(doc.toBson()); + return _pipeline + ? write_ops::UpdateModification(*_pipeline) + : write_ops::UpdateModification(doc.toBson(), + write_ops::UpdateModification::ClassicTag{}, + true /* isReplacement */); } /** @@ -201,7 +218,9 @@ private: return bob.obj(); } - void spill(BatchedObjects&& batch) override; + void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) override; + + BatchedCommandRequest initializeBatchedWriteRequest() const override; void waitWhileFailPointEnabled() override; diff --git a/src/mongo/db/pipeline/document_source_mock.h b/src/mongo/db/pipeline/document_source_mock.h index a6be10cbb84..39b3e17a265 100644 --- a/src/mongo/db/pipeline/document_source_mock.h +++ b/src/mongo/db/pipeline/document_source_mock.h @@ -106,7 +106,7 @@ public: * This stage does not modify anything. */ GetModPathsReturn getModifiedPaths() const override { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() override { diff --git a/src/mongo/db/pipeline/document_source_out.cpp b/src/mongo/db/pipeline/document_source_out.cpp index 9be7c7a24ba..9b826fb3c89 100644 --- a/src/mongo/db/pipeline/document_source_out.cpp +++ b/src/mongo/db/pipeline/document_source_out.cpp @@ -177,6 +177,11 @@ void DocumentSourceOut::finalize() { _tempNs = {}; } +BatchedCommandRequest DocumentSourceOut::initializeBatchedWriteRequest() const { + // Note that our insert targets '_tempNs' since we will never write to 'outputNs' directly. + return DocumentSourceWriter::makeInsertCommand(_tempNs, pExpCtx->bypassDocumentValidation); +} + boost::intrusive_ptr<DocumentSource> DocumentSourceOut::create( NamespaceString outputNs, const boost::intrusive_ptr<ExpressionContext>& expCtx) { diff --git a/src/mongo/db/pipeline/document_source_out.h b/src/mongo/db/pipeline/document_source_out.h index 64dda167eb3..0be5153cdf0 100644 --- a/src/mongo/db/pipeline/document_source_out.h +++ b/src/mongo/db/pipeline/document_source_out.h @@ -121,19 +121,25 @@ private: void finalize() override; - void spill(BatchedObjects&& batch) override { + void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) override { DocumentSourceWriteBlock writeBlock(pExpCtx->opCtx); + auto insertCommand = bcr.extractInsertRequest(); + insertCommand->setDocuments(std::move(batch)); auto targetEpoch = boost::none; + uassertStatusOK(pExpCtx->mongoProcessInterface->insert( - pExpCtx, _tempNs, std::move(batch), _writeConcern, targetEpoch)); + pExpCtx, _tempNs, std::move(insertCommand), _writeConcern, targetEpoch)); } std::pair<BSONObj, int> makeBatchObject(Document&& doc) const override { auto obj = doc.toBson(); - return {obj, obj.objsize()}; + tassert(6628900, "_writeSizeEstimator should be initialized", _writeSizeEstimator); + return {obj, _writeSizeEstimator->estimateInsertSizeBytes(obj)}; } + BatchedCommandRequest initializeBatchedWriteRequest() const override; + void waitWhileFailPointEnabled() override; // Holds on to the original collection options and index specs so we can check they didn't diff --git a/src/mongo/db/pipeline/document_source_queue.h b/src/mongo/db/pipeline/document_source_queue.h index 31dc128f6cb..e7eb6452d06 100644 --- a/src/mongo/db/pipeline/document_source_queue.h +++ b/src/mongo/db/pipeline/document_source_queue.h @@ -75,7 +75,7 @@ public: * This stage does not modify anything. */ GetModPathsReturn getModifiedPaths() const override { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } /** diff --git a/src/mongo/db/pipeline/document_source_redact.h b/src/mongo/db/pipeline/document_source_redact.h index 10fcfc14f63..b566f2c7adf 100644 --- a/src/mongo/db/pipeline/document_source_redact.h +++ b/src/mongo/db/pipeline/document_source_redact.h @@ -72,6 +72,16 @@ public: return _expression; } + DepsTracker::State getDependencies(DepsTracker* deps) const final { + // Add the dependencies of the expression but all we really care about is variable + // references for correlation analysis. The field references may get populated but we'll + // still require the full document since the $redact may descend arbitrary levels of nested + // documents that is only known at runtime. + _expression->addDependencies(deps); + deps->needWholeDocument = true; + return DepsTracker::State::SEE_NEXT; + } + private: DocumentSourceRedact(const boost::intrusive_ptr<ExpressionContext>& expCtx, const boost::intrusive_ptr<Expression>& previsit); diff --git a/src/mongo/db/pipeline/document_source_redact_test.cpp b/src/mongo/db/pipeline/document_source_redact_test.cpp index 95d001e4dac..6605796138b 100644 --- a/src/mongo/db/pipeline/document_source_redact_test.cpp +++ b/src/mongo/db/pipeline/document_source_redact_test.cpp @@ -81,5 +81,26 @@ TEST_F(DocumentSourceRedactTest, ShouldPropagatePauses) { ASSERT_TRUE(redact->getNext().isEOF()); ASSERT_TRUE(redact->getNext().isEOF()); } + +TEST_F(DocumentSourceRedactTest, ReportsVariableDependencies) { + auto varId = getExpCtx()->variablesParseState.defineVariable("var"); + auto redactSpec = fromjson(R"({ + "$redact" : { + "$cond" : { + "if" : "$$var", + "then" : "$$PRUNE", + "else" : "$$DESCEND" + } + } + })"); + auto redact = DocumentSourceRedact::createFromBson(redactSpec.firstElement(), getExpCtx()); + + DepsTracker deps; + ASSERT_EQ(redact->getDependencies(&deps), DepsTracker::State::SEE_NEXT); + ASSERT_EQ(deps.needWholeDocument, true); + ASSERT_TRUE(deps.fields.empty()); + ASSERT_EQ(deps.vars.count(varId), 1); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_replace_root.h b/src/mongo/db/pipeline/document_source_replace_root.h index e302ce5d88c..f288aa10321 100644 --- a/src/mongo/db/pipeline/document_source_replace_root.h +++ b/src/mongo/db/pipeline/document_source_replace_root.h @@ -74,7 +74,7 @@ public: DocumentSource::GetModPathsReturn getModifiedPaths() const final { // Replaces the entire root, so all paths are modified. - return {DocumentSource::GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; } const boost::intrusive_ptr<Expression>& getExpression() const { diff --git a/src/mongo/db/pipeline/document_source_sample.cpp b/src/mongo/db/pipeline/document_source_sample.cpp index 5ec204d1fa9..24e86b38cba 100644 --- a/src/mongo/db/pipeline/document_source_sample.cpp +++ b/src/mongo/db/pipeline/document_source_sample.cpp @@ -52,8 +52,10 @@ REGISTER_DOCUMENT_SOURCE(sample, AllowedWithApiStrict::kAlways); DocumentSource::GetNextResult DocumentSourceSample::doGetNext() { - if (_size == 0) + if (_size == 0) { + pSource->dispose(); return GetNextResult::makeEOF(); + } if (!_sortStage->isPopulated()) { // Exhaust source stage, add random metadata, and push all into sorter. diff --git a/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp b/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp index 33c32f096b3..39a13272df6 100644 --- a/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp +++ b/src/mongo/db/pipeline/document_source_sequential_document_cache.cpp @@ -41,7 +41,6 @@ DocumentSourceSequentialDocumentCache::DocumentSourceSequentialDocumentCache( const boost::intrusive_ptr<ExpressionContext>& expCtx, SequentialDocumentCache* cache) : DocumentSource(kStageName, expCtx), _cache(cache) { invariant(_cache); - invariant(!_cache->isAbandoned()); if (_cache->isServing()) { _cache->restartIteration(); @@ -96,7 +95,7 @@ Pipeline::SourceContainer::iterator DocumentSourceSequentialDocumentCache::doOpt _hasOptimizedPos = true; // If the cache is the only stage in the pipeline, return immediately. - if (itr == container->begin()) { + if (itr == container->begin() && std::next(itr) == container->end()) { return container->end(); } diff --git a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h index 0fccb527fad..1fea0368017 100644 --- a/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h +++ b/src/mongo/db/pipeline/document_source_set_variable_from_subpipeline.h @@ -76,6 +76,7 @@ public: } // This stage doesn't modify documents. setVariableConstraints.preservesOrderAndMetadata = true; + setVariableConstraints.canSwapWithSkippingOrLimitingStage = true; return setVariableConstraints; } diff --git a/src/mongo/db/pipeline/document_source_set_window_fields.cpp b/src/mongo/db/pipeline/document_source_set_window_fields.cpp index 45223bb1cf4..73806992561 100644 --- a/src/mongo/db/pipeline/document_source_set_window_fields.cpp +++ b/src/mongo/db/pipeline/document_source_set_window_fields.cpp @@ -464,8 +464,12 @@ DocumentSource::GetNextResult DocumentSourceInternalSetWindowFields::doGetNext() return DocumentSource::GetNextResult::makeEOF(); auto curDoc = _iterator.current(); - // The only way we hit this case is if there are no documents, since otherwise _eof will be set. if (!curDoc) { + if (_iterator.isPaused()) { + return DocumentSource::GetNextResult::makePauseExecution(); + } + // The only way we hit this case is if there are no documents, since otherwise _eof will be + // set. _eof = true; return DocumentSource::GetNextResult::makeEOF(); } diff --git a/src/mongo/db/pipeline/document_source_set_window_fields.h b/src/mongo/db/pipeline/document_source_set_window_fields.h index 652d1206ec9..f0f9b0742ce 100644 --- a/src/mongo/db/pipeline/document_source_set_window_fields.h +++ b/src/mongo/db/pipeline/document_source_set_window_fields.h @@ -117,7 +117,7 @@ public: _iterator(expCtx.get(), pSource, &_memoryTracker, std::move(partitionBy), _sortBy){}; GetModPathsReturn getModifiedPaths() const final { - std::set<std::string> outputPaths; + OrderedPathSet outputPaths; for (auto&& outputField : _outputFields) { outputPaths.insert(outputField.fieldName); } diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp new file mode 100644 index 00000000000..ff9326ceae4 --- /dev/null +++ b/src/mongo/db/pipeline/document_source_sharded_data_distribution.cpp @@ -0,0 +1,129 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/document_source_sharded_data_distribution.h" + +#include "mongo/db/pipeline/document_source_group.h" +#include "mongo/db/pipeline/document_source_internal_all_collection_stats.h" +#include "mongo/db/pipeline/document_source_lookup.h" +#include "mongo/db/pipeline/document_source_match.h" +#include "mongo/db/pipeline/document_source_project.h" +#include "mongo/db/pipeline/expression_context.h" +#include "mongo/db/pipeline/lite_parsed_document_source.h" +#include "mongo/s/catalog/type_collection.h" + +namespace mongo { + +using boost::intrusive_ptr; +using std::list; + +REGISTER_DOCUMENT_SOURCE(shardedDataDistribution, + DocumentSourceShardedDataDistribution::LiteParsed::parse, + DocumentSourceShardedDataDistribution::createFromBson, + AllowedWithApiStrict::kAlways); + +list<intrusive_ptr<DocumentSource>> DocumentSourceShardedDataDistribution::createFromBson( + BSONElement elem, const intrusive_ptr<ExpressionContext>& expCtx) { + uassert(6789100, + "The $shardedDataDistribution stage specification must be an empty object", + elem.type() == Object && elem.Obj().isEmpty()); + + uassert( + 6789101, "The $shardedDataDistribution stage can only be run on mongoS", expCtx->inMongos); + + uassert(6789102, + "The $shardedDataDistribution stage must be run on the admin database", + expCtx->ns.isAdminDB() && expCtx->ns.isCollectionlessAggregateNS()); + + static const BSONObj kAllCollStatsObj = + fromjson("{$_internalAllCollectionStats: {stats: {storageStats: {}}}}}"); + static const BSONObj kGroupObj = fromjson(R"({ + $group: { + _id: "$ns", + shards: { + $push: { + $let: { + vars: { + nOwnedDocs: { + $subtract: [ + "$storageStats.count", + "$storageStats.numOrphanDocs" + ] + } + }, + in: { + shardName: "$shard", + numOrphanedDocs: "$storageStats.numOrphanDocs", + numOwnedDocuments: "$$nOwnedDocs", + ownedSizeBytes: { + $multiply: [ + "$storageStats.avgObjSize", + "$$nOwnedDocs" + ] + }, + orphanedSizeBytes: { + $multiply: [ + "$storageStats.avgObjSize", + "$storageStats.numOrphanDocs" + ] + } + } + } + } + } + } + })"); + static const BSONObj kLookupObj = fromjson(R"({ + $lookup: { + from: { + db: "config", + coll: "collections" + }, + localField: "_id", + foreignField: "_id", + as: "matchingShardedCollection" + } + })"); + static const BSONObj kMatchObj = fromjson("{$match: {matchingShardedCollection: {$ne: []}}}"); + static const BSONObj kProjectObj = fromjson(R"({ + $project: { + _id: 0, + ns: "$_id", + shards: "$shards" + } + })"); + + return {DocumentSourceInternalAllCollectionStats::createFromBsonInternal( + kAllCollStatsObj.firstElement(), expCtx), + DocumentSourceGroup::createFromBson(kGroupObj.firstElement(), expCtx), + DocumentSourceLookUp::createFromBson(kLookupObj.firstElement(), expCtx), + DocumentSourceMatch::createFromBson(kMatchObj.firstElement(), expCtx), + DocumentSourceProject::createFromBson(kProjectObj.firstElement(), expCtx)}; +} +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sharded_data_distribution.h b/src/mongo/db/pipeline/document_source_sharded_data_distribution.h new file mode 100644 index 00000000000..23705cf6aeb --- /dev/null +++ b/src/mongo/db/pipeline/document_source_sharded_data_distribution.h @@ -0,0 +1,73 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/pipeline/document_source.h" + +namespace mongo { + +/** + * This aggregation stage is an alias for ‘$shardedDataDistribution’. It takes no arguments. Its + * response will be a cursor, each document of which represents the data-distribution information + * for a particular collection. + */ +namespace DocumentSourceShardedDataDistribution { + +static constexpr StringData kStageName = "$shardedDataDistribution"_sd; + +class LiteParsed final : public LiteParsedDocumentSource { +public: + static std::unique_ptr<LiteParsed> parse(const NamespaceString& nss, const BSONElement& spec) { + return std::make_unique<LiteParsed>(spec.fieldName()); + } + + explicit LiteParsed(std::string parseTimeName) + : LiteParsedDocumentSource(std::move(parseTimeName)) {} + + stdx::unordered_set<NamespaceString> getInvolvedNamespaces() const final { + return {NamespaceString::kConfigsvrCollectionsNamespace}; + } + + PrivilegeVector requiredPrivileges(bool isMongos, bool bypassDocumentValidation) const final { + return { + Privilege(ResourcePattern::forClusterResource(), ActionType::shardedDataDistribution)}; + } + + bool isInitialSource() const final { + return true; + } +}; + +static std::list<boost::intrusive_ptr<DocumentSource>> createFromBson( + BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); + +}; // namespace DocumentSourceShardedDataDistribution + +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_sort.cpp b/src/mongo/db/pipeline/document_source_sort.cpp index 2c07c43c501..84b28d6fde9 100644 --- a/src/mongo/db/pipeline/document_source_sort.cpp +++ b/src/mongo/db/pipeline/document_source_sort.cpp @@ -142,13 +142,19 @@ REGISTER_DOCUMENT_SOURCE(sort, LiteParsedDocumentSourceDefault::parse, DocumentSourceSort::createFromBson, AllowedWithApiStrict::kAlways); + REGISTER_DOCUMENT_SOURCE_CONDITIONALLY( _internalBoundedSort, LiteParsedDocumentSourceDefault::parse, DocumentSourceSort::parseBoundedSort, - AllowedWithApiStrict::kNeverInVersion1, - AllowedWithClientType::kAny, - feature_flags::gFeatureFlagBucketUnpackWithSort.getVersion(), + ::mongo::getTestCommandsEnabled() ? AllowedWithApiStrict::kNeverInVersion1 + : AllowedWithApiStrict::kInternal, + ::mongo::getTestCommandsEnabled() ? AllowedWithClientType::kAny + : AllowedWithClientType::kInternal, + // We don't expect mongos or clients to produce this stage: + // We only generate it after multiplanning, which means only within one mongod process. + // So, we should be allowed to parse this stage regardless of FCV. + boost::none /*minVersion*/, feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV()); DocumentSource::GetNextResult::ReturnStatus DocumentSourceSort::timeSorterPeek() { @@ -301,8 +307,8 @@ void DocumentSourceSort::serializeToArray( if (explain >= ExplainOptions::Verbosity::kExecStats) { mutDoc["totalDataSizeSortedBytesEstimate"] = Value(static_cast<long long>(_timeSorter->totalDataSizeBytes())); - mutDoc["usedDisk"] = Value(_timeSorter->numSpills() > 0); - mutDoc["spills"] = Value(static_cast<long long>(_timeSorter->numSpills())); + mutDoc["usedDisk"] = Value(_timeSorter->stats().spilledRanges() > 0); + mutDoc["spills"] = Value(static_cast<long long>(_timeSorter->stats().spilledRanges())); } array.push_back(Value{mutDoc.freeze()}); @@ -482,6 +488,10 @@ intrusive_ptr<DocumentSourceSort> DocumentSourceSort::parseBoundedSort( BSONElement key = args["sortKey"]; uassert(6369904, "$_internalBoundedSort sortKey must be an object", key.type() == Object); + // Empty sort pattern is not allowed for the bounded sort. + uassert(6900501, + "$_internalBoundedSort stage must have at least one sort key", + !key.embeddedObject().isEmpty()); SortPattern pat{key.embeddedObject(), expCtx}; { @@ -649,7 +659,7 @@ boost::optional<DocumentSource::DistributedPlanLogic> DocumentSourceSort::distri } bool DocumentSourceSort::canRunInParallelBeforeWriteStage( - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const { + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const { // This is an interesting special case. If there are no further stages which require merging the // streams into one, a $sort should not require it. This is only the case because the sort order // doesn't matter for a pipeline ending with a write stage. We may encounter it here as an diff --git a/src/mongo/db/pipeline/document_source_sort.h b/src/mongo/db/pipeline/document_source_sort.h index f0ab0c28a3c..ba087ee4a3f 100644 --- a/src/mongo/db/pipeline/document_source_sort.h +++ b/src/mongo/db/pipeline/document_source_sort.h @@ -79,7 +79,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // A $sort does not modify any paths. - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } StageConstraints constraints(Pipeline::SplitState) const final { @@ -102,7 +102,7 @@ public: boost::optional<DistributedPlanLogic> distributedPlanLogic() final; bool canRunInParallelBeforeWriteStage( - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const final; + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const final; /** * Returns the sort key pattern. diff --git a/src/mongo/db/pipeline/document_source_streaming_group.cpp b/src/mongo/db/pipeline/document_source_streaming_group.cpp new file mode 100644 index 00000000000..44702ab550d --- /dev/null +++ b/src/mongo/db/pipeline/document_source_streaming_group.cpp @@ -0,0 +1,279 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/platform/basic.h" + +#include <memory> + +#include "mongo/db/exec/document_value/document.h" +#include "mongo/db/exec/document_value/value.h" +#include "mongo/db/exec/document_value/value_comparator.h" +#include "mongo/db/pipeline/accumulation_statement.h" +#include "mongo/db/pipeline/accumulator.h" +#include "mongo/db/pipeline/document_source_group.h" +#include "mongo/db/pipeline/document_source_streaming_group.h" +#include "mongo/db/pipeline/expression.h" +#include "mongo/db/pipeline/expression_context.h" +#include "mongo/db/pipeline/lite_parsed_document_source.h" +#include "mongo/db/stats/resource_consumption_metrics.h" +#include "mongo/util/destructor_guard.h" + +namespace mongo { + +/* + * $_internalStreamingGroup is an internal stage that is only used in certain cases by the + * pipeline optimizer. For now it should not be used anywhere outside the MongoDB server. + */ +REGISTER_DOCUMENT_SOURCE(_internalStreamingGroup, + LiteParsedDocumentSourceDefault::parse, + DocumentSourceStreamingGroup::createFromBson, + AllowedWithApiStrict::kAlways); + +constexpr StringData DocumentSourceStreamingGroup::kStageName; + +const char* DocumentSourceStreamingGroup::getSourceName() const { + return kStageName.rawData(); +} + +DocumentSource::GetNextResult DocumentSourceStreamingGroup::doGetNext() { + auto getReadyResult = getNextReadyGroup(); + if (!getReadyResult.isEOF()) { + return getReadyResult; + } else if (_sourceDepleted) { + dispose(); + return getReadyResult; + } + + auto prepareResult = readyNextBatch(); + if (prepareResult.isPaused()) { + return prepareResult; + } + return getNextReadyGroup(); +} + +DocumentSourceStreamingGroup::DocumentSourceStreamingGroup( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes) + : DocumentSourceGroupBase(kStageName, expCtx, maxMemoryUsageBytes), _sourceDepleted(false) {} + +boost::intrusive_ptr<DocumentSourceStreamingGroup> DocumentSourceStreamingGroup::create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const boost::intrusive_ptr<Expression>& groupByExpression, + std::vector<size_t> monotonicExpressionIndexes, + std::vector<AccumulationStatement> accumulationStatements, + boost::optional<size_t> maxMemoryUsageBytes) { + boost::intrusive_ptr<DocumentSourceStreamingGroup> groupStage = + new DocumentSourceStreamingGroup(expCtx, maxMemoryUsageBytes); + groupStage->setIdExpression(groupByExpression); + for (auto&& statement : accumulationStatements) { + groupStage->addAccumulator(statement); + } + uassert(7026709, + "streaming group must have at least one monotonic id expression", + !monotonicExpressionIndexes.empty()); + uassert(7026710, + "streaming group monotonic expression indexes must correspond to id expressions", + std::all_of(monotonicExpressionIndexes.begin(), + monotonicExpressionIndexes.end(), + [&](size_t i) { return i < groupStage->_idExpressions.size(); })); + groupStage->_monotonicExpressionIndexes = std::move(monotonicExpressionIndexes); + return groupStage; +} + +boost::intrusive_ptr<DocumentSource> DocumentSourceStreamingGroup::createFromBson( + BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx) { + return createFromBsonWithMaxMemoryUsage(std::move(elem), expCtx, boost::none); +} + +boost::intrusive_ptr<DocumentSource> DocumentSourceStreamingGroup::createFromBsonWithMaxMemoryUsage( + BSONElement elem, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes) { + boost::intrusive_ptr<DocumentSourceStreamingGroup> groupStage = + new DocumentSourceStreamingGroup(expCtx, maxMemoryUsageBytes); + groupStage->initializeFromBson(elem); + + const auto& monotonicIdFieldsElem = elem.Obj().getField(kMonotonicIdFieldsSpecField); + uassert(7026702, + "streaming group must specify an array of monotonic id fields " + + kMonotonicIdFieldsSpecField, + monotonicIdFieldsElem.type() == Array); + const auto& monotonicIdFields = monotonicIdFieldsElem.Array(); + if (groupStage->_idFieldNames.empty()) { + uassert(7026703, + "if there is no explicit id fields, " + kMonotonicIdFieldsSpecField + + " must contain a single \"_id\" string", + monotonicIdFields.size() == 1 && + monotonicIdFields[0].valueStringDataSafe() == "_id"_sd); + groupStage->_monotonicExpressionIndexes.push_back(0); + } else { + groupStage->_monotonicExpressionIndexes.reserve(monotonicIdFields.size()); + for (const auto& fieldNameElem : monotonicIdFields) { + uassert(7026704, + kMonotonicIdFieldsSpecField + " elements must be strings", + fieldNameElem.type() == String); + StringData fieldName = fieldNameElem.valueStringData(); + auto it = std::find( + groupStage->_idFieldNames.begin(), groupStage->_idFieldNames.end(), fieldName); + uassert(7026705, "id field not found", it != groupStage->_idFieldNames.end()); + groupStage->_monotonicExpressionIndexes.push_back( + std::distance(groupStage->_idFieldNames.begin(), it)); + } + std::sort(groupStage->_monotonicExpressionIndexes.begin(), + groupStage->_monotonicExpressionIndexes.end()); + } + + return groupStage; +} + +void DocumentSourceStreamingGroup::serializeAdditionalFields( + MutableDocument& out, boost::optional<ExplainOptions::Verbosity> explain) const { + std::vector<Value> monotonicIdFields; + if (_idFieldNames.empty()) { + monotonicIdFields.emplace_back("_id"_sd); + } else { + for (size_t i : _monotonicExpressionIndexes) { + monotonicIdFields.emplace_back(_idFieldNames[i]); + } + } + out[kMonotonicIdFieldsSpecField] = Value(std::move(monotonicIdFields)); +} + +bool DocumentSourceStreamingGroup::isSpecFieldReserved(StringData fieldName) { + return fieldName == kMonotonicIdFieldsSpecField; +} + +DocumentSource::GetNextResult DocumentSourceStreamingGroup::getNextDocument() { + if (_firstDocumentOfNextBatch) { + GetNextResult result = std::move(_firstDocumentOfNextBatch.value()); + _firstDocumentOfNextBatch.reset(); + return result; + } + return pSource->getNext(); +} + +DocumentSource::GetNextResult DocumentSourceStreamingGroup::readyNextBatch() { + resetReadyGroups(); + GetNextResult input = getNextDocument(); + return readyNextBatchInner(input); +} + +// This separate NOINLINE function is used here to decrease stack utilization of readyNextBatch() +// and prevent stack overflows. +MONGO_COMPILER_NOINLINE DocumentSource::GetNextResult +DocumentSourceStreamingGroup::readyNextBatchInner(GetNextResult input) { + setExecutionStarted(); + // Calculate groups until we either exaust pSource or encounter change in monotonic id + // expression, which means all current groups are finalized. + for (; input.isAdvanced(); input = pSource->getNext()) { + if (shouldSpillWithAttemptToSaveMemory()) { + spill(); + } + auto root = input.releaseDocument(); + Value id = computeId(root); + + if (isBatchFinished(id)) { + _firstDocumentOfNextBatch = std::move(root); + readyGroups(); + return input; + } + + processDocument(id, root); + } + + switch (input.getStatus()) { + case DocumentSource::GetNextResult::ReturnStatus::kAdvanced: { + MONGO_UNREACHABLE; // We consumed all advances above. + } + case DocumentSource::GetNextResult::ReturnStatus::kPauseExecution: { + return input; // Propagate pause. + } + case DocumentSource::GetNextResult::ReturnStatus::kEOF: { + readyGroups(); + _sourceDepleted = true; + return input; + } + } + MONGO_UNREACHABLE; +} + +bool DocumentSourceStreamingGroup::isBatchFinished(const Value& id) { + if (_idExpressions.size() == 1) { + tassert(7026706, + "if there are no explicit id fields, it is only one monotonic expression with id 0", + _monotonicExpressionIndexes.size() == 1 && _monotonicExpressionIndexes[0] == 0); + return checkForBatchEndAndUpdateLastIdValues([&](size_t) { return id; }); + } else { + tassert(7026707, + "if there are explicit id fields, internal representation of id is an array", + id.isArray()); + const std::vector<Value>& idValues = id.getArray(); + return checkForBatchEndAndUpdateLastIdValues([&](size_t i) { return idValues[i]; }); + } +} + +template <typename IdValueGetter> +bool DocumentSourceStreamingGroup::checkForBatchEndAndUpdateLastIdValues( + const IdValueGetter& idValueGetter) { + auto assertStreamable = [&](Value value) { + // Nullish and array values will mess us up because they sort differently than they group. + // A null and a missing value will compare equal in sorting, but could result in different + // groups, e.g. {_id: {x: null, y: null}} vs {_id: {}}. An array value will sort by the min + // or max element, with no tie breaking, but group by the whole array. This means that two + // of the exact same array could appear in the input sequence, but with a different array in + // the middle of them, and that would still be considered sorted. That would break our + // batching group logic. + uassert(7026708, + "Monotonic value should not be missing, null or an array", + !value.nullish() && !value.isArray()); + return value; + }; + + // If _lastMonotonicIdFieldValues is empty, it is the first document, so the only thing we need + // to do is initialize it. + if (_lastMonotonicIdFieldValues.empty()) { + for (size_t i : _monotonicExpressionIndexes) { + _lastMonotonicIdFieldValues.push_back(assertStreamable(idValueGetter(i))); + } + return false; + } else { + bool batchFinished = false; + for (size_t index = 0; index < _monotonicExpressionIndexes.size(); ++index) { + Value& oldId = _lastMonotonicIdFieldValues[index]; + const Value& id = assertStreamable(idValueGetter(_monotonicExpressionIndexes[index])); + if (pExpCtx->getValueComparator().compare(oldId, id) != 0) { + oldId = id; + batchFinished = true; + } + } + return batchFinished; + } +} + +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_streaming_group.h b/src/mongo/db/pipeline/document_source_streaming_group.h new file mode 100644 index 00000000000..bd72a3a7e7d --- /dev/null +++ b/src/mongo/db/pipeline/document_source_streaming_group.h @@ -0,0 +1,126 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <memory> +#include <utility> + +#include "mongo/db/pipeline/document_source_group_base.h" + +namespace mongo { + +/** + * This class represents streaming group implementation that can only be used when at least one of + * _id fields is monotonic. It stores and output groups in batches. All groups in the batch has + * the same value of monotonic id fields. + * + * For example, if the inputs are sorted by "x", we could use a batched streaming algorithm to + * perform the grouping for {$group: {_id: {x: "$x", y: "$y"}}}. + * + * Groups are processes in batches. One batch corresponds to a set of groups when each monotonic + * id field have the same value. Non-monotonic fields can have different values, so we still may + * have multiple groups and even spill to disk, but we still consume significanty less memory + * than general hash based group. + * When a document with a different value in at least one group id field is encountered, it is + * cached in '_firstDocumentOfNextBatch', current groups are finalized and returned in + * subsequent getNext() called and when the current batch is depleted, memory is freeed and the + * process starts again. + * + * TODO SERVER-71437 Implement an optimization for a special case where all group fields are + * monotonic + * - we don't need any hashing in this case. + */ +class DocumentSourceStreamingGroup final : public DocumentSourceGroupBase { +public: + static constexpr StringData kStageName = "$_internalStreamingGroup"_sd; + + const char* getSourceName() const final; + + /** + * Convenience method for creating a new $_internalStreamingGroup stage. If maxMemoryUsageBytes + * is boost::none, then it will actually use the value of + * internalDocumentSourceGroupMaxMemoryBytes. + */ + static boost::intrusive_ptr<DocumentSourceStreamingGroup> create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const boost::intrusive_ptr<Expression>& groupByExpression, + std::vector<size_t> monotonicExpressionIndexes, + std::vector<AccumulationStatement> accumulationStatements, + boost::optional<size_t> maxMemoryUsageBytes = boost::none); + + /** + * Parses 'elem' into a $_internalStreamingGroup stage, or throws a AssertionException if 'elem' + * was an invalid specification. + */ + static boost::intrusive_ptr<DocumentSource> createFromBson( + BSONElement elem, const boost::intrusive_ptr<ExpressionContext>& expCtx); + static boost::intrusive_ptr<DocumentSource> createFromBsonWithMaxMemoryUsage( + BSONElement elem, + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes); + +protected: + GetNextResult doGetNext() final; + + bool isSpecFieldReserved(StringData fieldName) final; + void serializeAdditionalFields(MutableDocument& out, + boost::optional<ExplainOptions::Verbosity> explain) const final; + +private: + static constexpr StringData kMonotonicIdFieldsSpecField = "$monotonicIdFields"_sd; + + explicit DocumentSourceStreamingGroup( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + boost::optional<size_t> maxMemoryUsageBytes = boost::none); + + + GetNextResult getNextDocument(); + + GetNextResult readyNextBatch(); + /** + * Readies next batch after all children are initialized. See readyNextBatch() for + * more details. + */ + GetNextResult readyNextBatchInner(GetNextResult input); + + bool isBatchFinished(const Value& id); + + template <typename IdValueGetter> + bool checkForBatchEndAndUpdateLastIdValues(const IdValueGetter& idValueGetter); + + std::vector<size_t> _monotonicExpressionIndexes; + std::vector<Value> _lastMonotonicIdFieldValues; + + boost::optional<Document> _firstDocumentOfNextBatch; + + bool _sourceDepleted; +}; + +} // namespace mongo diff --git a/src/mongo/db/pipeline/document_source_unwind.cpp b/src/mongo/db/pipeline/document_source_unwind.cpp index daf7adcd5ed..2161b0b9bbb 100644 --- a/src/mongo/db/pipeline/document_source_unwind.cpp +++ b/src/mongo/db/pipeline/document_source_unwind.cpp @@ -220,7 +220,7 @@ DocumentSource::GetNextResult DocumentSourceUnwind::doGetNext() { } DocumentSource::GetModPathsReturn DocumentSourceUnwind::getModifiedPaths() const { - std::set<std::string> modifiedFields{_unwindPath.fullPath()}; + OrderedPathSet modifiedFields{_unwindPath.fullPath()}; if (_indexPath) { modifiedFields.insert(_indexPath->fullPath()); } diff --git a/src/mongo/db/pipeline/document_source_writer.h b/src/mongo/db/pipeline/document_source_writer.h index a94b4efca6b..25fd08aac5c 100644 --- a/src/mongo/db/pipeline/document_source_writer.h +++ b/src/mongo/db/pipeline/document_source_writer.h @@ -36,8 +36,11 @@ #include "mongo/db/db_raii.h" #include "mongo/db/operation_context.h" #include "mongo/db/pipeline/document_source.h" +#include "mongo/db/query/query_knobs_gen.h" #include "mongo/db/read_concern.h" #include "mongo/db/storage/recovery_unit.h" +#include "mongo/rpc/metadata/impersonated_user_metadata.h" +#include "mongo/s/write_ops/batched_command_request.h" namespace mongo { using namespace fmt::literals; @@ -83,11 +86,14 @@ public: /** * This is a base abstract class for all stages performing a write operation into an output * collection. The writes are organized in batches in which elements are objects of the templated - * type 'B'. A subclass must override two methods to be able to write into the output collection: + * type 'B'. A subclass must override the following methods to be able to write into the output + * collection: * - * 1. 'makeBatchObject()' - to create an object of type 'B' from the given 'Document', which is, + * - 'makeBatchObject()' - creates an object of type 'B' from the given 'Document', which is, * essentially, a result of the input source's 'getNext()' . - * 2. 'spill()' - to write the batch into the output collection. + * - 'spill()' - writes the batch into the output collection. + * - 'initializeBatchedWriteRequest()' - initializes the request object for writing a batch to + * the output collection. * * Two other virtual methods exist which a subclass may override: 'initialize()' and 'finalize()', * which are called before the first element is read from the input source, and after the last one @@ -99,12 +105,26 @@ public: using BatchObject = B; using BatchedObjects = std::vector<BatchObject>; + static BatchedCommandRequest makeInsertCommand(const NamespaceString& outputNs, + bool bypassDocumentValidation) { + write_ops::InsertCommandRequest insertOp(outputNs); + insertOp.setWriteCommandRequestBase([&] { + write_ops::WriteCommandRequestBase wcb; + wcb.setOrdered(false); + wcb.setBypassDocumentValidation(bypassDocumentValidation); + return wcb; + }()); + return BatchedCommandRequest(std::move(insertOp)); + } + DocumentSourceWriter(const char* stageName, NamespaceString outputNs, const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSource(stageName, expCtx), _outputNs(std::move(outputNs)), - _writeConcern(expCtx->opCtx->getWriteConcern()) {} + _writeConcern(expCtx->opCtx->getWriteConcern()), + _writeSizeEstimator( + expCtx->mongoProcessInterface->getWriteSizeEstimator(expCtx->opCtx, outputNs)) {} DepsTracker::State getDependencies(DepsTracker* deps) const override { deps->needWholeDocument = true; @@ -114,7 +134,7 @@ public: GetModPathsReturn getModifiedPaths() const override { // For purposes of tracking which fields come from where, the writer stage does not modify // any fields by default. - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {}}; } boost::optional<DistributedPlanLogic> distributedPlanLogic() override { @@ -122,7 +142,7 @@ public: } bool canRunInParallelBeforeWriteStage( - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) const override { + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) const override { return true; } @@ -143,9 +163,31 @@ protected: virtual void finalize() {} /** - * Writes the documents in 'batch' to the output namespace. + * Writes the documents in 'batch' to the output namespace via 'bcr'. + */ + virtual void spill(BatchedCommandRequest&& bcr, BatchedObjects&& batch) = 0; + + /** + * Estimates the size of the header of a batch write (that is, the size of the write command + * minus the size of write statements themselves). + */ + int estimateWriteHeaderSize(const BatchedCommandRequest& bcr) const { + using BatchType = BatchedCommandRequest::BatchType; + switch (bcr.getBatchType()) { + case BatchType::BatchType_Insert: + return _writeSizeEstimator->estimateInsertHeaderSize(bcr.getInsertRequest()); + case BatchType::BatchType_Update: + return _writeSizeEstimator->estimateUpdateHeaderSize(bcr.getUpdateRequest()); + case BatchType::BatchType_Delete: + break; + } + MONGO_UNREACHABLE; + } + + /** + * Constructs and configures a BatchedCommandRequest for performing a batch write. */ - virtual void spill(BatchedObjects&& batch) = 0; + virtual BatchedCommandRequest initializeBatchedWriteRequest() const = 0; /** * Creates a batch object from the given document and returns it to the caller along with the @@ -169,6 +211,9 @@ protected: // respect the writeConcern of the original command. WriteConcernOptions _writeConcern; + // An interface that is used to estimate the size of each write operation. + const std::unique_ptr<MongoProcessInterface::WriteSizeEstimator> _writeSizeEstimator; + private: bool _initialized{false}; bool _done{false}; @@ -198,9 +243,30 @@ DocumentSource::GetNextResult DocumentSourceWriter<B>::doGetNext() { _initialized = true; } - BatchedObjects batch; - int bufferedBytes = 0; + // While most metadata attached to a command is limited to less than a KB, Impersonation + // metadata may grow to an arbitrary size. + // + // Ask the active Client how much impersonation metadata we'll use for it, add in our own + // estimate of write header size, and assume that the rest can fit in the space reserved by + // BSONObjMaxUserSize's overhead plus the value from the server parameter: + // internalQueryDocumentSourceWriterBatchExtraReservedBytes. + const auto estimatedMetadataSizeBytes = + rpc::estimateImpersonatedUserMetadataSize(pExpCtx->opCtx); + BatchedCommandRequest batchWrite = initializeBatchedWriteRequest(); + const auto writeHeaderSize = estimateWriteHeaderSize(batchWrite); + const auto initialRequestSize = estimatedMetadataSizeBytes + writeHeaderSize + + internalQueryDocumentSourceWriterBatchExtraReservedBytes.load(); + + uassert(7637800, + "Unable to proceed with write while metadata size ({}KB) exceeds {}KB"_format( + initialRequestSize / 1024, BSONObjMaxUserSize / 1024), + initialRequestSize <= BSONObjMaxUserSize); + + const auto maxBatchSizeBytes = BSONObjMaxUserSize - initialRequestSize; + + BatchedObjects batch; + size_t bufferedBytes = 0; auto nextInput = pSource->getNext(); for (; nextInput.isAdvanced(); nextInput = pSource->getNext()) { waitWhileFailPointEnabled(); @@ -210,16 +276,17 @@ DocumentSource::GetNextResult DocumentSourceWriter<B>::doGetNext() { bufferedBytes += objSize; if (!batch.empty() && - (bufferedBytes > BSONObjMaxUserSize || + (bufferedBytes > maxBatchSizeBytes || batch.size() >= write_ops::kMaxWriteBatchSize)) { - spill(std::move(batch)); + spill(std::move(batchWrite), std::move(batch)); batch.clear(); + batchWrite = initializeBatchedWriteRequest(); bufferedBytes = objSize; } batch.push_back(obj); } if (!batch.empty()) { - spill(std::move(batch)); + spill(std::move(batchWrite), std::move(batch)); batch.clear(); } diff --git a/src/mongo/db/pipeline/expression.cpp b/src/mongo/db/pipeline/expression.cpp index bf0915deddd..ecb1f2382de 100644 --- a/src/mongo/db/pipeline/expression.cpp +++ b/src/mongo/db/pipeline/expression.cpp @@ -188,7 +188,7 @@ void Expression::registerExpression( parserMap[key] = ParserRegistration{parser, allowedWithApiStrict, allowedWithClientType, requiredMinVersion}; // Add this expression to the global map of operator counters for expressions. - operatorCountersAggExpressions.addAggExpressionCounter(key); + operatorCountersAggExpressions.addCounter(key); } intrusive_ptr<Expression> Expression::parseExpression(ExpressionContext* const expCtx, @@ -308,111 +308,204 @@ const char* ExpressionAbs::getOpName() const { /* ------------------------- ExpressionAdd ----------------------------- */ -StatusWith<Value> ExpressionAdd::apply(Value lhs, Value rhs) { - BSONType diffType = Value::getWidestNumeric(rhs.getType(), lhs.getType()); - - if (diffType == NumberDecimal) { - Decimal128 left = lhs.coerceToDecimal(); - Decimal128 right = rhs.coerceToDecimal(); - return Value(left.add(right)); - } else if (diffType == NumberDouble) { - double right = rhs.coerceToDouble(); - double left = lhs.coerceToDouble(); - return Value(left + right); - } else if (diffType == NumberLong) { - long long result; +namespace { - // If there is an overflow, convert the values to doubles. - if (overflow::add(lhs.coerceToLong(), rhs.coerceToLong(), &result)) { - return Value(lhs.coerceToDouble() + rhs.coerceToDouble()); +/** + * We'll try to return the narrowest possible result value while avoiding overflow or implicit use + * of decimal types. To do that, compute separate sums for long, double and decimal values, and + * track the current widest type. The long sum will be converted to double when the first double + * value is seen or when long arithmetic would overflow. + */ +class AddState { +public: + /** + * Update the internal state with another operand. It is up to the caller to validate that the + * operand is of a proper type. + */ + void operator+=(const Value& operand) { + auto oldWidestType = widestType; + // Dates are represented by the long number of milliseconds since the unix epoch, so we can + // treat them as regular numeric values for the purposes of addition after making sure that + // only one date is present in the operand list. + Value valToAdd; + if (operand.getType() == Date) { + uassert(16612, "only one date allowed in an $add expression", !isDate); + Value oldValue = getValue(); + longTotal = 0; + addToDateValue(oldValue); + isDate = true; + valToAdd = Value(operand.getDate().toMillisSinceEpoch()); + } else { + widestType = Value::getWidestNumeric(widestType, operand.getType()); + valToAdd = operand; } - return Value(result); - } else if (diffType == NumberInt) { - long long right = rhs.coerceToLong(); - long long left = lhs.coerceToLong(); - return Value::createIntOrLong(left + right); - } else if (lhs.nullish() || rhs.nullish()) { - return Value(BSONNULL); - } else { - return Status(ErrorCodes::TypeMismatch, - str::stream() << "cannot $add a" << typeName(rhs.getType()) << " from a " - << typeName(lhs.getType())); - } -} -Value ExpressionAdd::evaluate(const Document& root, Variables* variables) const { - // We'll try to return the narrowest possible result value while avoiding overflow, loss - // of precision due to intermediate rounding or implicit use of decimal types. To do that, - // compute a compensated sum for non-decimal values and a separate decimal sum for decimal - // values, and track the current narrowest type. - DoubleDoubleSummation nonDecimalTotal; - Decimal128 decimalTotal; - BSONType totalType = NumberInt; - bool haveDate = false; + if (isDate) { + addToDateValue(valToAdd); + return; + } - const size_t n = _children.size(); - for (size_t i = 0; i < n; ++i) { - Value val = _children[i]->evaluate(root, variables); + // If this operation widens the return type, perform any necessary type conversions. + if (oldWidestType != widestType) { + switch (widestType) { + case NumberLong: + // Int -> Long is handled by the same sum. + break; + case NumberDouble: + // Int/Long -> Double converts the existing longTotal to a doubleTotal. + doubleTotal = longTotal; + break; + case NumberDecimal: + // Convert the right total to NumberDecimal by looking at the old widest type. + switch (oldWidestType) { + case NumberInt: + case NumberLong: + decimalTotal = Decimal128(longTotal); + break; + case NumberDouble: + decimalTotal = Decimal128(doubleTotal); + break; + default: + MONGO_UNREACHABLE; + } + break; + default: + MONGO_UNREACHABLE; + } + } - switch (val.getType()) { - case NumberDecimal: - decimalTotal = decimalTotal.add(val.getDecimal()); - totalType = NumberDecimal; + // Perform the add operation. + switch (widestType) { + case NumberInt: + case NumberLong: + // If the long long arithmetic overflows, promote the result to a NumberDouble and + // start incrementing the doubleTotal. + long long newLongTotal; + if (overflow::add(longTotal, valToAdd.coerceToLong(), &newLongTotal)) { + widestType = NumberDouble; + doubleTotal = longTotal + valToAdd.coerceToDouble(); + } else { + longTotal = newLongTotal; + } break; case NumberDouble: - nonDecimalTotal.addDouble(val.getDouble()); - if (totalType != NumberDecimal) - totalType = NumberDouble; + doubleTotal += valToAdd.coerceToDouble(); break; - case NumberLong: - nonDecimalTotal.addLong(val.getLong()); - if (totalType == NumberInt) - totalType = NumberLong; + case NumberDecimal: + decimalTotal = decimalTotal.add(valToAdd.coerceToDecimal()); break; + default: + uasserted(ErrorCodes::TypeMismatch, + str::stream() << "$add only supports numeric or date types, not " + << typeName(valToAdd.getType())); + } + } + + Value getValue() const { + // If one of the operands was a date, then return long value as Date. + if (isDate) { + return Value(Date_t::fromMillisSinceEpoch(longTotal)); + } else { + switch (widestType) { + case NumberInt: + return Value::createIntOrLong(longTotal); + case NumberLong: + return Value(longTotal); + case NumberDouble: + return Value(doubleTotal); + case NumberDecimal: + return Value(decimalTotal); + default: + MONGO_UNREACHABLE; + } + } + } + +private: + // Convert 'valToAdd' into the data type used for dates (long long) and add it to 'longTotal'. + void addToDateValue(Value valToAdd) { + switch (valToAdd.getType()) { case NumberInt: - nonDecimalTotal.addDouble(val.getInt()); + case NumberLong: + if (overflow::add(longTotal, valToAdd.coerceToLong(), &longTotal)) { + uasserted(ErrorCodes::Overflow, "date overflow"); + } break; - case Date: - uassert(16612, "only one date allowed in an $add expression", !haveDate); - haveDate = true; - nonDecimalTotal.addLong(val.getDate().toMillisSinceEpoch()); + case NumberDouble: { + using limits = std::numeric_limits<long long>; + double doubleToAdd = valToAdd.coerceToDouble(); + uassert(ErrorCodes::Overflow, + "date overflow", + // The upper bound is exclusive because it rounds up when it is cast to + // a double. + doubleToAdd >= static_cast<double>(limits::min()) && + doubleToAdd < static_cast<double>(limits::max())); + + if (overflow::add(longTotal, llround(doubleToAdd), &longTotal)) { + uasserted(ErrorCodes::Overflow, "date overflow"); + } + break; + } + case NumberDecimal: { + Decimal128 decimalToAdd = valToAdd.coerceToDecimal(); + + std::uint32_t signalingFlags = Decimal128::SignalingFlag::kNoFlag; + std::int64_t longToAdd = decimalToAdd.toLong(&signalingFlags); + if (signalingFlags != Decimal128::SignalingFlag::kNoFlag || + overflow::add(longTotal, longToAdd, &longTotal)) { + uasserted(ErrorCodes::Overflow, "date overflow"); + } break; + } default: - uassert(16554, - str::stream() << "$add only supports numeric or date types, not " - << typeName(val.getType()), - val.nullish()); - return Value(BSONNULL); + MONGO_UNREACHABLE; } } - if (haveDate) { - int64_t longTotal; - if (totalType == NumberDecimal) { - longTotal = decimalTotal.add(nonDecimalTotal.getDecimal()).toLong(); - } else { - uassert(ErrorCodes::Overflow, "date overflow in $add", nonDecimalTotal.fitsLong()); - longTotal = nonDecimalTotal.getLong(); - } - return Value(Date_t::fromMillisSinceEpoch(longTotal)); + long long longTotal = 0; + double doubleTotal = 0; + Decimal128 decimalTotal; + BSONType widestType = NumberInt; + bool isDate = false; +}; + +Status checkAddOperandType(Value val) { + if (!val.numeric() && val.getType() != Date) { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "$add only supports numeric or date types, not " + << typeName(val.getType())); } - switch (totalType) { - case NumberDecimal: - return Value(decimalTotal.add(nonDecimalTotal.getDecimal())); - case NumberLong: - dassert(nonDecimalTotal.isInteger()); - if (nonDecimalTotal.fitsLong()) - return Value(nonDecimalTotal.getLong()); - // Fallthrough. - case NumberInt: - if (nonDecimalTotal.fitsLong()) - return Value::createIntOrLong(nonDecimalTotal.getLong()); - // Fallthrough. - case NumberDouble: - return Value(nonDecimalTotal.getDouble()); - default: - massert(16417, "$add resulted in a non-numeric type", false); + + return Status::OK(); +} +} // namespace + +StatusWith<Value> ExpressionAdd::apply(Value lhs, Value rhs) { + if (lhs.nullish()) + return Value(BSONNULL); + if (Status s = checkAddOperandType(lhs); !s.isOK()) + return s; + if (rhs.nullish()) + return Value(BSONNULL); + if (Status s = checkAddOperandType(rhs); !s.isOK()) + return s; + + AddState state; + state += lhs; + state += rhs; + return state.getValue(); +} + +Value ExpressionAdd::evaluate(const Document& root, Variables* variables) const { + AddState state; + for (auto&& child : _children) { + Value val = child->evaluate(root, variables); + if (val.nullish()) + return Value(BSONNULL); + uassertStatusOK(checkAddOperandType(val)); + state += val; } + return state.getValue(); } REGISTER_STABLE_EXPRESSION(add, ExpressionAdd::parse); @@ -2134,6 +2227,17 @@ void ExpressionDateDiff::_doAddDependencies(DepsTracker* deps) const { } } +monotonic::State ExpressionDateDiff::getMonotonicState(const FieldPath& sortedFieldPath) const { + if (!ExpressionConstant::allNullOrConstant({_unit, _timeZone, _startOfWeek})) { + return monotonic::State::NonMonotonic; + } + // Because the result of this expression can be negative, this works the same way as + // ExpressionSubtract. Edge cases with DST and other timezone changes are handled correctly + // according to dateDiff. + return monotonic::combine(_endDate->getMonotonicState(sortedFieldPath), + monotonic::opposite(_startDate->getMonotonicState(sortedFieldPath))); +} + /* ----------------------- ExpressionDivide ---------------------------- */ Value ExpressionDivide::evaluate(const Document& root, Variables* variables) const { @@ -2507,6 +2611,11 @@ std::unique_ptr<Expression> ExpressionFieldPath::copyWithSubstitution( return nullptr; } +monotonic::State ExpressionFieldPath::getMonotonicState(const FieldPath& sortedFieldPath) const { + return getFieldPathWithoutCurrentPrefix() == sortedFieldPath ? monotonic::State::Increasing + : monotonic::State::NonMonotonic; +} + /* ------------------------- ExpressionFilter ----------------------------- */ REGISTER_STABLE_EXPRESSION(filter, ExpressionFilter::parse); @@ -2983,9 +3092,11 @@ const std::string sortKeyName = "sortKey"; const std::string searchScoreDetailsName = "searchScoreDetails"; const std::string timeseriesBucketMinTimeName = "timeseriesBucketMinTime"; const std::string timeseriesBucketMaxTimeName = "timeseriesBucketMaxTime"; +const std::string vectorSearchScoreName = "vectorSearchScore"; using MetaType = DocumentMetadataFields::MetaType; const StringMap<DocumentMetadataFields::MetaType> kMetaNameToMetaType = { + {vectorSearchScoreName, MetaType::kVectorSearchScore}, {geoNearDistanceName, MetaType::kGeoNearDist}, {geoNearPointName, MetaType::kGeoNearPoint}, {indexKeyName, MetaType::kIndexKey}, @@ -3001,6 +3112,7 @@ const StringMap<DocumentMetadataFields::MetaType> kMetaNameToMetaType = { }; const stdx::unordered_map<DocumentMetadataFields::MetaType, StringData> kMetaTypeToMetaName = { + {MetaType::kVectorSearchScore, vectorSearchScoreName}, {MetaType::kGeoNearDist, geoNearDistanceName}, {MetaType::kGeoNearPoint, geoNearPointName}, {MetaType::kIndexKey, indexKeyName}, @@ -3044,6 +3156,9 @@ Value ExpressionMeta::serialize(bool explain) const { Value ExpressionMeta::evaluate(const Document& root, Variables* variables) const { const auto& metadata = root.metadata(); switch (_metaType) { + case MetaType::kVectorSearchScore: + return metadata.hasVectorSearchScore() ? Value(metadata.getVectorSearchScore()) + : Value(); case MetaType::kTextScore: return metadata.hasTextScore() ? Value(metadata.getTextScore()) : Value(); case MetaType::kRandVal: @@ -5535,14 +5650,45 @@ StatusWith<Value> ExpressionSubtract::apply(Value lhs, Value rhs) { } else if (lhs.nullish() || rhs.nullish()) { return Value(BSONNULL); } else if (lhs.getType() == Date) { - if (rhs.getType() == Date) { - return Value(durationCount<Milliseconds>(lhs.getDate() - rhs.getDate())); - } else if (rhs.numeric()) { - return Value(lhs.getDate() - Milliseconds(rhs.coerceToLong())); - } else { - return Status(ErrorCodes::TypeMismatch, - str::stream() - << "can't $subtract " << typeName(rhs.getType()) << " from Date"); + BSONType rhsType = rhs.getType(); + switch (rhsType) { + case Date: + return Value(durationCount<Milliseconds>(lhs.getDate() - rhs.getDate())); + case NumberInt: + case NumberLong: { + long long longDiff = lhs.getDate().toMillisSinceEpoch(); + if (overflow::sub(longDiff, rhs.coerceToLong(), &longDiff)) { + return Status(ErrorCodes::Overflow, str::stream() << "date overflow"); + } + return Value(Date_t::fromMillisSinceEpoch(longDiff)); + } + case NumberDouble: { + using limits = std::numeric_limits<long long>; + long long longDiff = lhs.getDate().toMillisSinceEpoch(); + double doubleRhs = rhs.coerceToDouble(); + // check the doubleRhs should not exceed int64 limit and result will not overflow + if (doubleRhs >= static_cast<double>(limits::min()) && + doubleRhs < static_cast<double>(limits::max()) && + !overflow::sub(longDiff, llround(doubleRhs), &longDiff)) { + return Value(Date_t::fromMillisSinceEpoch(longDiff)); + } + return Status(ErrorCodes::Overflow, str::stream() << "date overflow"); + } + case NumberDecimal: { + long long longDiff = lhs.getDate().toMillisSinceEpoch(); + Decimal128 decimalRhs = rhs.coerceToDecimal(); + std::uint32_t signalingFlags = Decimal128::SignalingFlag::kNoFlag; + std::int64_t longRhs = decimalRhs.toLong(&signalingFlags); + if (signalingFlags != Decimal128::SignalingFlag::kNoFlag || + overflow::sub(longDiff, longRhs, &longDiff)) { + return Status(ErrorCodes::Overflow, str::stream() << "date overflow"); + } + return Value(Date_t::fromMillisSinceEpoch(longDiff)); + } + default: + return Status(ErrorCodes::TypeMismatch, + str::stream() + << "can't $subtract " << typeName(rhs.getType()) << " from Date"); } } else { return Status(ErrorCodes::TypeMismatch, @@ -5556,24 +5702,35 @@ const char* ExpressionSubtract::getOpName() const { return "$subtract"; } +monotonic::State ExpressionSubtract::getMonotonicState(const FieldPath& sortedFieldPath) const { + // 1. Get monotonic states of the both children. + // 2. Apply monotonic::opposite to the state of the second child, because it is negated. + // 3. Combine children. Function monotonic::combine correctly handles all the cases where, for + // example, argumemnts are both monotonic, but in the opposite directions. + return monotonic::combine( + getChildren()[0]->getMonotonicState(sortedFieldPath), + monotonic::opposite(getChildren()[1]->getMonotonicState(sortedFieldPath))); +} + /* ------------------------- ExpressionSwitch ------------------------------ */ REGISTER_STABLE_EXPRESSION(switch, ExpressionSwitch::parse); Value ExpressionSwitch::evaluate(const Document& root, Variables* variables) const { - for (auto&& branch : _branches) { - Value caseExpression(branch.first->evaluate(root, variables)); + for (int i = 0; i < numBranches(); ++i) { + auto [caseExpr, thenExpr] = getBranch(i); + Value caseResult = caseExpr->evaluate(root, variables); - if (caseExpression.coerceToBool()) { - return branch.second->evaluate(root, variables); + if (caseResult.coerceToBool()) { + return thenExpr->evaluate(root, variables); } } uassert(40066, "$switch could not find a matching branch for an input, and no default was specified.", - _default); + defaultExpr()); - return _default->evaluate(root, variables); + return defaultExpr()->evaluate(root, variables); } boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* const expCtx, @@ -5582,7 +5739,7 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* cons uassert(40060, str::stream() << "$switch requires an object as an argument, found: " << typeName(expr.type()), - expr.type() == Object); + expr.type() == BSONType::Object); boost::intrusive_ptr<Expression> expDefault; std::vector<boost::intrusive_ptr<Expression>> children; @@ -5594,13 +5751,13 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* cons uassert(40061, str::stream() << "$switch expected an array for 'branches', found: " << typeName(elem.type()), - elem.type() == Array); + elem.type() == BSONType::Array); for (auto&& branch : elem.Array()) { uassert(40062, str::stream() << "$switch expected each branch to be an object, found: " << typeName(branch.type()), - branch.type() == Object); + branch.type() == BSONType::Object); boost::intrusive_ptr<Expression> switchCase, switchThen; @@ -5632,80 +5789,77 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::parse(ExpressionContext* cons uasserted(40067, str::stream() << "$switch found an unknown argument: " << field); } } + + // The the 'default' expression is always the final child. If no 'default' expression is + // provided, then the final child is nullptr. children.push_back(std::move(expDefault)); - // Obtain references to the case and branch expressions two-by-two from the children vector, - // ignore the last. - std::vector<ExpressionPair> branches; - boost::optional<boost::intrusive_ptr<Expression>&> first; - for (auto&& child : children) { - if (first) { - branches.emplace_back(*first, child); - first = boost::none; - } else { - first = child; - } - } - uassert(40068, "$switch requires at least one branch.", !branches.empty()); + return new ExpressionSwitch(expCtx, std::move(children)); +} - return new ExpressionSwitch(expCtx, std::move(children), std::move(branches)); +void ExpressionSwitch::deleteBranch(int i) { + invariant(i >= 0); + invariant(i < numBranches()); + // Delete the two elements corresponding to this branch at positions 2i and 2i + 1. + _children.erase(std::next(_children.begin(), i * 2), std::next(_children.begin(), i * 2 + 2)); } void ExpressionSwitch::_doAddDependencies(DepsTracker* deps) const { - for (auto&& branch : _branches) { - branch.first->addDependencies(deps); - branch.second->addDependencies(deps); - } - - if (_default) { - _default->addDependencies(deps); + for (auto&& child : _children) { + // Check for nullptr, since we leave a nullptr as the final child when the 'default' + // expression is missing. + if (child) { + child->addDependencies(deps); + } } } boost::intrusive_ptr<Expression> ExpressionSwitch::optimize() { - if (_default) { - _default = _default->optimize(); + if (defaultExpr()) { + _children.back() = _children.back()->optimize(); } - std::vector<ExpressionPair>::iterator it = _branches.begin(); - bool true_const = false; + bool trueConst = false; - while (!true_const && it != _branches.end()) { - (it->first) = (it->first)->optimize(); + int i = 0; + while (!trueConst && i < numBranches()) { + boost::intrusive_ptr<Expression>& caseExpr = _children[i * 2]; + boost::intrusive_ptr<Expression>& thenExpr = _children[i * 2 + 1]; + caseExpr = caseExpr->optimize(); - if (auto* val = dynamic_cast<ExpressionConstant*>((it->first).get())) { - if (!((val->getValue()).coerceToBool())) { + if (auto* val = dynamic_cast<ExpressionConstant*>(caseExpr.get())) { + if (!val->getValue().coerceToBool()) { // Case is constant and evaluates to false, so it is removed. - it = _branches.erase(it); + deleteBranch(i); } else { - // Case is constant and true so it is set to default and then removed. - true_const = true; - - // Optimizing this case's then, so that default will remain optimized. - (it->second) = (it->second)->optimize(); - _default = it->second; - it = _branches.erase(it); + // Case optimized to a constant true value. Set the optimized version of the + // corresponding 'then' expression as the new 'default'. Break out of the loop and + // fall through to the logic to remove this and all subsequent branches. + trueConst = true; + _children.back() = thenExpr->optimize(); + break; } } else { // Since case is not removed from the switch, its then is now optimized. - (it->second) = (it->second)->optimize(); - ++it; + thenExpr = thenExpr->optimize(); + ++i; } } // Erasing the rest of the cases because found a default true value. - if (true_const) { - _branches.erase(it, _branches.end()); + if (trueConst) { + while (i < numBranches()) { + deleteBranch(i); + } } // If there are no cases, make the switch its default. - if (_branches.size() == 0 && _default) { - return _default; - } else if (_branches.size() == 0) { + if (numBranches() == 0) { uassert(40069, - "One cannot execute a switch statement where all the cases evaluate to false " - "without a default.", - _branches.size()); + "Cannot execute a switch statement where all the cases evaluate to false " + "without a default", + defaultExpr()); + return _children.back(); } return this; @@ -5713,17 +5867,18 @@ boost::intrusive_ptr<Expression> ExpressionSwitch::optimize() { Value ExpressionSwitch::serialize(bool explain) const { std::vector<Value> serializedBranches; - serializedBranches.reserve(_branches.size()); + serializedBranches.reserve(numBranches()); - for (auto&& branch : _branches) { - serializedBranches.push_back(Value(Document{{"case", branch.first->serialize(explain)}, - {"then", branch.second->serialize(explain)}})); + for (int i = 0; i < numBranches(); ++i) { + auto [caseExpr, thenExpr] = getBranch(i); + serializedBranches.push_back(Value(Document{{"case", caseExpr->serialize(explain)}, + {"then", thenExpr->serialize(explain)}})); } - if (_default) { + if (defaultExpr()) { return Value(Document{{"$switch", Document{{"branches", Value(serializedBranches)}, - {"default", _default->serialize(explain)}}}}); + {"default", defaultExpr()->serialize(explain)}}}}); } return Value(Document{{"$switch", Document{{"branches", Value(serializedBranches)}}}}); @@ -7551,6 +7706,15 @@ Value ExpressionDateArithmetics::evaluate(const Document& root, Variables* varia startDate.coerceToDate(), unit, amount.coerceToLong(), timezone.get()); } +monotonic::State ExpressionDateArithmetics::getMonotonicState( + const FieldPath& sortedFieldPath) const { + if (!ExpressionConstant::allNullOrConstant({_unit, _timeZone})) { + return monotonic::State::NonMonotonic; + } + return combineMonotonicStateOfArguments(_startDate->getMonotonicState(sortedFieldPath), + _amount->getMonotonicState(sortedFieldPath)); +} + /* ----------------------- ExpressionDateAdd ---------------------------- */ REGISTER_STABLE_EXPRESSION(dateAdd, ExpressionDateAdd::parse); @@ -7575,6 +7739,11 @@ Value ExpressionDateAdd::evaluateDateArithmetics(Date_t date, return Value(dateAdd(date, unit, amount, timezone)); } +monotonic::State ExpressionDateAdd::combineMonotonicStateOfArguments( + monotonic::State startDataMonotonicState, monotonic::State amountMonotonicState) const { + return monotonic::combine(startDataMonotonicState, amountMonotonicState); +} + /* ----------------------- ExpressionDateSubtract ---------------------------- */ REGISTER_STABLE_EXPRESSION(dateSubtract, ExpressionDateSubtract::parse); @@ -7604,6 +7773,11 @@ Value ExpressionDateSubtract::evaluateDateArithmetics(Date_t date, return Value(dateAdd(date, unit, -amount, timezone)); } +monotonic::State ExpressionDateSubtract::combineMonotonicStateOfArguments( + monotonic::State startDataMonotonicState, monotonic::State amountMonotonicState) const { + return monotonic::combine(startDataMonotonicState, amountMonotonicState); +} + /* ----------------------- ExpressionDateTrunc ---------------------------- */ REGISTER_STABLE_EXPRESSION(dateTrunc, ExpressionDateTrunc::parse); @@ -7776,6 +7950,13 @@ void ExpressionDateTrunc::_doAddDependencies(DepsTracker* deps) const { } } +monotonic::State ExpressionDateTrunc::getMonotonicState(const FieldPath& sortedFieldPath) const { + if (!ExpressionConstant::allNullOrConstant({_unit, _binSize, _timeZone, _startOfWeek})) { + return monotonic::State::NonMonotonic; + } + return _date->getMonotonicState(sortedFieldPath); +} + /* -------------------------- ExpressionGetField ------------------------------ */ REGISTER_EXPRESSION_WITH_MIN_VERSION( getField, diff --git a/src/mongo/db/pipeline/expression.h b/src/mongo/db/pipeline/expression.h index 9cce6d0b1e2..4ad28bf3d04 100644 --- a/src/mongo/db/pipeline/expression.h +++ b/src/mongo/db/pipeline/expression.h @@ -49,6 +49,7 @@ #include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/expression_visitor.h" #include "mongo/db/pipeline/field_path.h" +#include "mongo/db/pipeline/monotonic_expression.h" #include "mongo/db/pipeline/variables.h" #include "mongo/db/query/allowed_contexts.h" #include "mongo/db/query/datetime/date_time_support.h" @@ -170,7 +171,7 @@ public: */ struct ComputedPaths { // Non-rename computed paths. - std::set<std::string> paths; + OrderedPathSet paths; // Mappings from the old name of a path before applying this expression, to the new one // after applying this expression. @@ -339,6 +340,18 @@ public: return _expCtx; } + boost::optional<Variables::Id> getBoundaryVariableId() const { + return _boundaryVariableId; + } + + bool isMonotonic(const FieldPath& sortedFieldPath) const { + return getMonotonicState(sortedFieldPath) != monotonic::State::NonMonotonic; + } + + virtual monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const { + return monotonic::State::NonMonotonic; + } + protected: using ExpressionVector = std::vector<boost::intrusive_ptr<Expression>>; @@ -737,6 +750,10 @@ protected: void _doAddDependencies(DepsTracker* deps) const override; private: + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { + return monotonic::State::Constant; + } + Value _value; }; @@ -943,6 +960,11 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } + +private: + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { + return monotonic::combineExpressions(sortedFieldPath, getChildren()); + }; }; @@ -1188,6 +1210,11 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } + +private: + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { + return getChildren()[0]->getMonotonicState(sortedFieldPath); + } }; @@ -1662,6 +1689,8 @@ private: void _doAddDependencies(DepsTracker* deps) const final; + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; + // Starting time instant expression. Accepted types: Date_t, Timestamp, OID. boost::intrusive_ptr<Expression>& _startDate; @@ -1832,6 +1861,8 @@ protected: private: + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; + /* Internal implementation of evaluate(), used recursively. @@ -1923,6 +1954,11 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } + +private: + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final { + return getChildren()[0]->getMonotonicState(sortedFieldPath); + } }; @@ -2860,7 +2896,10 @@ public: } bool isCommutative() const final { - return true; + // Only commutative when performing binary string comparison. The first value entered when + // multiple collation-equal but binary-unequal values are added will dictate what is stored + // in the set. + return getExpressionContext()->getCollator() == nullptr; } void acceptVisitor(ExpressionMutableVisitor* visitor) final { @@ -3276,6 +3315,9 @@ public: void acceptVisitor(ExpressionConstVisitor* visitor) const final { return visitor->visit(this); } + +private: + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; }; @@ -3285,11 +3327,10 @@ public: std::pair<boost::intrusive_ptr<Expression>&, boost::intrusive_ptr<Expression>&>; ExpressionSwitch(ExpressionContext* const expCtx, - std::vector<boost::intrusive_ptr<Expression>> children, - std::vector<ExpressionPair> branches) - : Expression(expCtx, std::move(children)), - _default(_children.back()), - _branches(std::move(branches)) {} + std::vector<boost::intrusive_ptr<Expression>> children) + : Expression(expCtx, std::move(children)) { + uassert(40068, "$switch requires at least one branch", numBranches() >= 1); + } Value evaluate(const Document& root, Variables* variables) const final; boost::intrusive_ptr<Expression> optimize() final; @@ -3306,12 +3347,38 @@ public: return visitor->visit(this); } + /** + * Returns the number of cases in the switch expression. Each branch is made up of two + * expressions ('case' and 'then'). + */ + int numBranches() const { + return _children.size() / 2; + } + + /** + * Returns a pair of expression pointers representing the 'case' and 'then' expressions for the + * i-th branch of the switch. + */ + std::pair<const Expression*, const Expression*> getBranch(int i) const { + invariant(i >= 0); + invariant(i < numBranches()); + return {_children[i * 2].get(), _children[i * 2 + 1].get()}; + } + + /** + * Returns the 'default' expression, or nullptr if there is no 'default'. + */ + const Expression* defaultExpr() const { + return _children.back().get(); + } + protected: void _doAddDependencies(DepsTracker* deps) const final; private: - boost::intrusive_ptr<Expression>& _default; - std::vector<ExpressionPair> _branches; + // Helper for 'optimize()'. Deletes the 'case' and 'then' children associated with the i-th + // branch of the switch. + void deleteBranch(int i); }; @@ -3982,6 +4049,10 @@ protected: long long amount, const TimeZone& timezone) const = 0; + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; + virtual monotonic::State combineMonotonicStateOfArguments( + monotonic::State startDataMonotonicState, monotonic::State amountMonotonicState) const = 0; + private: // The expression representing the startDate argument. boost::intrusive_ptr<Expression>& _startDate; @@ -4016,10 +4087,14 @@ public: } private: - virtual Value evaluateDateArithmetics(Date_t date, - TimeUnit unit, - long long amount, - const TimeZone& timezone) const override; + monotonic::State combineMonotonicStateOfArguments( + monotonic::State startDataMonotonicState, + monotonic::State amountMonotonicState) const final; + + Value evaluateDateArithmetics(Date_t date, + TimeUnit unit, + long long amount, + const TimeZone& timezone) const final; }; class ExpressionDateSubtract final : public ExpressionDateArithmetics { @@ -4039,10 +4114,14 @@ public: } private: - virtual Value evaluateDateArithmetics(Date_t date, - TimeUnit unit, - long long amount, - const TimeZone& timezone) const override; + monotonic::State combineMonotonicStateOfArguments( + monotonic::State startDataMonotonicState, + monotonic::State amountMonotonicState) const final; + + Value evaluateDateArithmetics(Date_t date, + TimeUnit unit, + long long amount, + const TimeZone& timezone) const final; }; struct SubstituteFieldPathWalker { @@ -4128,6 +4207,8 @@ private: void _doAddDependencies(DepsTracker* deps) const final; + monotonic::State getMonotonicState(const FieldPath& sortedFieldPath) const final; + // Expression that evaluates to a date to truncate. Accepted BSON types: Date, bsonTimestamp, // jstOID. boost::intrusive_ptr<Expression>& _date; diff --git a/src/mongo/db/pipeline/expression_context.cpp b/src/mongo/db/pipeline/expression_context.cpp index 2c41c612595..7258456a1ac 100644 --- a/src/mongo/db/pipeline/expression_context.cpp +++ b/src/mongo/db/pipeline/expression_context.cpp @@ -251,11 +251,27 @@ void ExpressionContext::incrementAggExprCounter(StringData name) { } } +void ExpressionContext::incrementGroupAccumulatorExprCounter(StringData name) { + if (enabledCounters && _expressionCounters) { + ++_expressionCounters.get().groupAccumulatorExprCountersMap[name]; + } +} + +void ExpressionContext::incrementWindowAccumulatorExprCounter(StringData name) { + if (enabledCounters && _expressionCounters) { + ++_expressionCounters.get().windowAccumulatorExprCountersMap[name]; + } +} + void ExpressionContext::stopExpressionCounters() { if (enabledCounters && _expressionCounters) { operatorCountersMatchExpressions.mergeCounters( _expressionCounters.get().matchExprCountersMap); operatorCountersAggExpressions.mergeCounters(_expressionCounters.get().aggExprCountersMap); + operatorCountersGroupAccumulatorExpressions.mergeCounters( + _expressionCounters.get().groupAccumulatorExprCountersMap); + operatorCountersWindowAccumulatorExpressions.mergeCounters( + _expressionCounters.get().windowAccumulatorExprCountersMap); } _expressionCounters = boost::none; } diff --git a/src/mongo/db/pipeline/expression_context.h b/src/mongo/db/pipeline/expression_context.h index 3f83fba6c8e..38747fd60ae 100644 --- a/src/mongo/db/pipeline/expression_context.h +++ b/src/mongo/db/pipeline/expression_context.h @@ -60,15 +60,6 @@ namespace mongo { class AggregateCommandRequest; -/** - * The structure ExpressionCounters encapsulates counters for match, aggregate, and other - * expression types as seen in the end-user queries. - */ -struct ExpressionCounters { - StringMap<uint64_t> aggExprCountersMap; - StringMap<uint64_t> matchExprCountersMap; -}; - class ExpressionContext : public RefCountable { public: struct ResolvedNamespace { @@ -118,6 +109,8 @@ public: struct ExpressionCounters { StringMap<uint64_t> aggExprCountersMap; StringMap<uint64_t> matchExprCountersMap; + StringMap<uint64_t> groupAccumulatorExprCountersMap; + StringMap<uint64_t> windowAccumulatorExprCountersMap; }; /** @@ -380,6 +373,16 @@ public: void incrementAggExprCounter(StringData name); /** + * Increment the counter for the $group accumulator expression with a given name. + */ + void incrementGroupAccumulatorExprCounter(StringData name); + + /** + * Increment the counter for the $setWindowFields accumulator expression with a given name. + */ + void incrementWindowAccumulatorExprCounter(StringData name); + + /** * Merge expression counters from the current expression context into the global maps * and stop counting. */ @@ -433,7 +436,7 @@ public: // Tracks the depth of nested aggregation sub-pipelines. Used to enforce depth limits. long long subPipelineDepth = 0; - // True if this 'ExpressionContext' object is for the inner side of a $lookup. + // True if this 'ExpressionContext' object is for the inner side of a $lookup or $graphLookup. bool inLookup = false; // If set, this will disallow use of features introduced in versions above the provided version. @@ -481,11 +484,57 @@ public: // The resume token version that should be generated by a change stream. int changeStreamTokenVersion = ResumeTokenData::kDefaultTokenVersion; + // If set to true, always use 'changeStreamTokenVersion' when resuming a stream, regardless of + // the client resume token's version. + bool ignoreTokenVersionOnResume = false; + // True if the expression context is the original one for a given pipeline. // False if another context is created for the same pipeline. Used to disable duplicate // expression counting. bool enabledCounters = true; + // Sets or clears a flag which tells DocumentSource parsers whether any involved Collection + // may contain extended-range dates. + void setRequiresTimeseriesExtendedRangeSupport(bool v) { + _requiresTimeseriesExtendedRangeSupport = v; + } + bool getRequiresTimeseriesExtendedRangeSupport() const { + return _requiresTimeseriesExtendedRangeSupport; + } + + // This is state that is to be shared between the DocumentInternalSearchMongotRemote and + // DocumentInternalSearchIdLookup stages (these stages are the result of desugaring $search) + // during runtime. + class SharedSearchState { + public: + SharedSearchState() {} + + long long getDocsReturnedByIdLookup() const { + return _docsReturnedByIdLookup; + } + + /** + * Sets the value of _docsReturnedByIdLookup to 0. + */ + void resetDocsReturnedByIdLookup() { + _docsReturnedByIdLookup = 0; + } + + /** + * Increments the value of _docsReturnedByIdLookup by 1. + */ + void incrementDocsReturnedByIdLookup() { + _docsReturnedByIdLookup++; + } + + private: + // When there is an extractable limit in the query, DocumentInternalSearchMongotRemote sends + // a getMore to mongot that specifies how many more documents it needs to fulfill that + // limit, and it incorporates the amount of documents returned by the + // DocumentInternalSearchIdLookup stage into that value. + long long _docsReturnedByIdLookup = 0; + } sharedSearchState; + protected: static const int kInterruptCheckPeriod = 128; @@ -510,6 +559,8 @@ protected: bool _isCappedDelete = false; + bool _requiresTimeseriesExtendedRangeSupport = false; + private: boost::optional<ExpressionCounters> _expressionCounters = boost::none; }; diff --git a/src/mongo/db/pipeline/expression_date_test.cpp b/src/mongo/db/pipeline/expression_date_test.cpp index efe0a577ef6..9205ea29633 100644 --- a/src/mongo/db/pipeline/expression_date_test.cpp +++ b/src/mongo/db/pipeline/expression_date_test.cpp @@ -1771,7 +1771,7 @@ TEST_F(ExpressionDateDiffTest, AddsDependencies) { auto depsTracker = dateDiffExpression->getDependencies(); ASSERT_TRUE( (depsTracker.fields == - std::set<std::string>{ + OrderedPathSet{ "startDateField", "endDateField", "unitField", "timezoneField", "startOfWeekField"})); } } // namespace ExpressionDateDiffTest @@ -1869,7 +1869,7 @@ TEST_F(ExpressionDateTruncTest, AddsDependencies) { const auto depsTracker = dateTruncExpression->getDependencies(); ASSERT_TRUE( (depsTracker.fields == - std::set<std::string>{ + OrderedPathSet{ "dateField", "unitField", "binSizeField", "timezoneField", "startOfWeekField"})); } } // namespace diff --git a/src/mongo/db/pipeline/expression_js_emit.cpp b/src/mongo/db/pipeline/expression_js_emit.cpp index d19a9c53191..ca6ba238c6e 100644 --- a/src/mongo/db/pipeline/expression_js_emit.cpp +++ b/src/mongo/db/pipeline/expression_js_emit.cpp @@ -145,11 +145,14 @@ Value ExpressionInternalJsEmit::evaluate(const Document& root, Variables* variab ExpressionContext* expCtx = getExpressionContext(); auto jsExec = expCtx->getJsExecWithScope(); - // Inject the native "emit" function to be called from the user-defined map function. This - // particular Expression/ExpressionContext may be reattached to a new OperationContext (and thus - // a new JS Scope) when used across getMore operations, so this method will handle that case for - // us by only injecting if we haven't already. - jsExec->injectEmitIfNecessary(emitFromJS, &_emitState); + + // Inject the native "emit" function to be called from the user-defined map function. + // + // We reinject this function on every invocation of evaluate(), because there is a single + // JsExecution instance for the OperationContext, which may be shared by multiple aggregation + // pipelines and we need to ensure that the injected function still points to the valid + // contextual data ('_emitState'). + jsExec->injectEmit(emitFromJS, &_emitState); // Although inefficient to "create" a new function every time we evaluate, this will usually end // up being a simple cache lookup. This is needed because the JS Scope may have been recreated diff --git a/src/mongo/db/pipeline/expression_test.cpp b/src/mongo/db/pipeline/expression_test.cpp index fd6f1c3490e..03fc4d3b663 100644 --- a/src/mongo/db/pipeline/expression_test.cpp +++ b/src/mongo/db/pipeline/expression_test.cpp @@ -1121,7 +1121,7 @@ TEST(ExpressionSwitch, ExpressionSwitchWithAllConstantFalsesAndNoDefaultErrors) ASSERT_THROWS_CODE(switchExp->optimize(), AssertionException, 40069); } -TEST(ExpressionSwitch, ExpressionSwitchWithZeroAsConstantFalsesAndNoDefaulErrors) { +TEST(ExpressionSwitch, ExpressionSwitchWithZeroAsConstantFalseAndNoDefaultErrors) { auto expCtx = ExpressionContextForTest{}; VariablesParseState vps = expCtx.variablesParseState; @@ -1233,6 +1233,62 @@ TEST(ExpressionSwitch, ExpressionSwitchWithNoConstantsShouldStayTheSame) { ASSERT_BSONOBJ_BINARY_EQ(switchQ, expressionToBson(optimizedStaySame)); } +// This test was designed to provide coverage for SERVER-70190, a bug in which optimizing a $switch +// expression could leave its children vector in a bad state. By walking the tree after optimizing +// we make sure that the expected children are found. +TEST(ExpressionSwitch, CaseEliminationShouldLeaveTreeInWalkableState) { + auto expCtx = ExpressionContextForTest{}; + VariablesParseState vps = expCtx.variablesParseState; + + BSONObj switchQ = fromjson(R"( + {$switch: { + branches: [ + {case: false, then: {$const: 0}}, + {case: "$z", then: {$const: 1}}, + {case: "$y", then: {$const: 3}}, + {case: true, then: {$const: 4}}, + {case: "$a", then: {$const: 5}}, + {case: "$b", then: {$const: 6}}, + {case: "$c", then: {$const: 7}} + ], + default: {$const: 8} + }} + )"); + auto switchExp = ExpressionSwitch::parse(&expCtx, switchQ.firstElement(), vps); + auto optimizedExpr = switchExp->optimize(); + + BSONObj optimizedQ = fromjson(R"( + {$switch: { + branches: [ + {case: "$z", then: {$const: 1}}, + {case: "$y", then: {$const: 3}} + ], + default: {$const: 4} + }} + )"); + + ASSERT_BSONOBJ_BINARY_EQ(optimizedQ, expressionToBson(optimizedExpr)); + + // Make sure that the expression tree appears as expected when the children are traversed using + // a for-each loop. + int childNum = 0; + int numConstants = 0; + for (auto&& child : optimizedExpr->getChildren()) { + // Children 0 and 2 are field path expressions, whereas 1, 3, and 4 are constants. + auto constExpr = dynamic_cast<ExpressionConstant*>(child.get()); + if (constExpr) { + ASSERT_VALUE_EQ(constExpr->getValue(), Value{childNum}); + ++numConstants; + } else { + ASSERT(dynamic_cast<ExpressionFieldPath*>(child.get())); + } + ++childNum; + } + // We should have seen 5 children total, 3 of which are constants. + ASSERT_EQ(childNum, 5); + ASSERT_EQ(numConstants, 3); +} + TEST(ExpressionArray, ExpressionArrayShouldOptimizeSubExpressionToExpressionConstant) { auto expCtx = ExpressionContextForTest{}; VariablesParseState vps = expCtx.variablesParseState; @@ -3105,6 +3161,18 @@ TEST(ExpressionMetaTest, ExpressionMetaSearchScoreDetails) { Value val = expressionMeta->evaluate(doc.freeze(), &expCtx.variables); ASSERT_DOCUMENT_EQ(val.getDocument(), Document(details)); } + +TEST(ExpressionMetaTest, ExpressionMetaVectorSearchScore) { + auto expCtx = ExpressionContextForTest{}; + BSONObj expr = fromjson("{$meta: \"vectorSearchScore\"}"); + auto expressionMeta = + ExpressionMeta::parse(&expCtx, expr.firstElement(), expCtx.variablesParseState); + + MutableDocument doc; + doc.metadata().setVectorSearchScore(1.23); + Value val = expressionMeta->evaluate(doc.freeze(), &expCtx.variables); + ASSERT_EQ(val.getDouble(), 1.23); +} } // namespace expression_meta_test namespace ExpressionRegexTest { diff --git a/src/mongo/db/pipeline/field_path.cpp b/src/mongo/db/pipeline/field_path.cpp index ab00617bbd3..8839b906daf 100644 --- a/src/mongo/db/pipeline/field_path.cpp +++ b/src/mongo/db/pipeline/field_path.cpp @@ -54,7 +54,12 @@ const StringDataSet kAllowedDollarPrefixedFields = { "$sortKey", // This is necessary for the "showRecordId" feature. - "$recordId"}; + "$recordId", + + // This is necessary for $search queries with a specified sort. + "$searchSortValues"_sd, + "$searchScore"_sd, +}; } // namespace diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.cpp b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp new file mode 100644 index 00000000000..e1c711f30f3 --- /dev/null +++ b/src/mongo/db/pipeline/group_from_first_document_transformation.cpp @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/group_from_first_document_transformation.h" + +namespace mongo { +Document GroupFromFirstDocumentTransformation::applyTransformation(const Document& input) { + MutableDocument output(_accumulatorExprs.size()); + + for (auto&& expr : _accumulatorExprs) { + auto value = expr.second->evaluate(input, &expr.second->getExpressionContext()->variables); + output.addField(expr.first, value.missing() ? Value(BSONNULL) : std::move(value)); + } + + return output.freeze(); +} + +void GroupFromFirstDocumentTransformation::optimize() { + for (auto&& expr : _accumulatorExprs) { + expr.second = expr.second->optimize(); + } +} + +Document GroupFromFirstDocumentTransformation::serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const { + + MutableDocument newRoot(_accumulatorExprs.size()); + for (auto&& expr : _accumulatorExprs) { + newRoot.addField(expr.first, expr.second->serialize(static_cast<bool>(explain))); + } + + return {{"newRoot", newRoot.freezeToValue()}}; +} + +DepsTracker::State GroupFromFirstDocumentTransformation::addDependencies(DepsTracker* deps) const { + for (auto&& expr : _accumulatorExprs) { + expr.second->addDependencies(deps); + } + + // This stage will replace the entire document with a new document, so any existing fields + // will be replaced and cannot be required as dependencies. We use EXHAUSTIVE_ALL here + // instead of EXHAUSTIVE_FIELDS, as in ReplaceRootTransformation, because the stages that + // follow a $group stage should not depend on document metadata. + return DepsTracker::State::EXHAUSTIVE_ALL; +} + +DocumentSource::GetModPathsReturn GroupFromFirstDocumentTransformation::getModifiedPaths() const { + // Replaces the entire root, so all paths are modified. + return {DocumentSource::GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; +} + +std::unique_ptr<GroupFromFirstDocumentTransformation> GroupFromFirstDocumentTransformation::create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const std::string& groupId, + StringData originalStageName, + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs) { + return std::make_unique<GroupFromFirstDocumentTransformation>( + groupId, originalStageName, std::move(accumulatorExprs)); +} + +} // namespace mongo diff --git a/src/mongo/db/pipeline/group_from_first_document_transformation.h b/src/mongo/db/pipeline/group_from_first_document_transformation.h new file mode 100644 index 00000000000..541cd87967c --- /dev/null +++ b/src/mongo/db/pipeline/group_from_first_document_transformation.h @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/pipeline/expression.h" +#include "mongo/db/pipeline/transformer_interface.h" + +namespace mongo { + +/** + * GroupFromFirstTransformation consists of a list of (field name, expression pairs). It returns a + * document synthesized by assigning each field name in the output document to the result of + * evaluating the corresponding expression. If the expression evaluates to missing, we assign a + * value of BSONNULL. This is necessary to match the semantics of $first for missing fields. + */ +class GroupFromFirstDocumentTransformation final : public TransformerInterface { +public: + GroupFromFirstDocumentTransformation( + const std::string& groupId, + StringData originalStageName, + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs) + : _accumulatorExprs(std::move(accumulatorExprs)), + _groupId(groupId), + _originalStageName(originalStageName) {} + + TransformerType getType() const final { + return TransformerType::kGroupFromFirstDocument; + } + + /** + * The path of the field that we are grouping on: i.e., the field in the input document that we + * will use to create the _id field of the ouptut document. + */ + const std::string& groupId() const { + return _groupId; + } + + StringData originalStageName() const { + return _originalStageName; + } + + Document applyTransformation(const Document& input) final; + + void optimize() final; + + Document serializeTransformation( + boost::optional<ExplainOptions::Verbosity> explain) const final; + + DepsTracker::State addDependencies(DepsTracker* deps) const final; + + DocumentSource::GetModPathsReturn getModifiedPaths() const final; + + static std::unique_ptr<GroupFromFirstDocumentTransformation> create( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const std::string& groupId, + StringData originalStageName, + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> accumulatorExprs); + +private: + std::vector<std::pair<std::string, boost::intrusive_ptr<Expression>>> _accumulatorExprs; + std::string _groupId; + StringData _originalStageName; +}; + +} // namespace mongo diff --git a/src/mongo/db/pipeline/javascript_execution.h b/src/mongo/db/pipeline/javascript_execution.h index 924ae447216..d95e90f6eea 100644 --- a/src/mongo/db/pipeline/javascript_execution.h +++ b/src/mongo/db/pipeline/javascript_execution.h @@ -104,11 +104,8 @@ public: * Injects the given function 'emitFn' as a native JS function named 'emit', callable from * user-defined functions. */ - void injectEmitIfNecessary(NativeFunction emitFn, void* data) { - if (!_emitCreated) { - _scope->injectNative("emit", emitFn, data); - _emitCreated = true; - } + void injectEmit(NativeFunction emitFn, void* data) { + _scope->injectNative("emit", emitFn, data); } Scope* getScope() { @@ -118,7 +115,6 @@ public: private: BSONObj _scopeVars; std::unique_ptr<Scope> _scope; - bool _emitCreated = false; bool _storedProceduresLoaded = false; int _fnCallTimeoutMillis; diff --git a/src/mongo/db/pipeline/lite_parsed_document_source.h b/src/mongo/db/pipeline/lite_parsed_document_source.h index 34648576917..aed1d4fefcf 100644 --- a/src/mongo/db/pipeline/lite_parsed_document_source.h +++ b/src/mongo/db/pipeline/lite_parsed_document_source.h @@ -171,6 +171,20 @@ public: } /** + * Returns true if this is a $changeStreamSplitLargeEvent stage. + */ + virtual bool isChangeStreamSplitLargeEvent() const { + return false; + } + + /** + * Returns true if this is a $documents stage. + */ + virtual bool isDocuments() const { + return false; + } + + /** * Returns true if this stage does not require an input source. */ virtual bool isInitialSource() const { diff --git a/src/mongo/db/pipeline/lite_parsed_pipeline.cpp b/src/mongo/db/pipeline/lite_parsed_pipeline.cpp index 36b1d78d9b7..f5760429206 100644 --- a/src/mongo/db/pipeline/lite_parsed_pipeline.cpp +++ b/src/mongo/db/pipeline/lite_parsed_pipeline.cpp @@ -157,7 +157,6 @@ void LiteParsedPipeline::tickGlobalStageCounters() const { void LiteParsedPipeline::validate(const OperationContext* opCtx, bool performApiVersionChecks) const { - int internalUnpackBucketCount = 0; for (auto&& stage : _stageSpecs) { const auto& stageName = stage->getParseTimeName(); const auto& stageInfo = LiteParsedDocumentSource::getInfo(stageName); @@ -179,25 +178,10 @@ void LiteParsedPipeline::validate(const OperationContext* opCtx, sometimesCallback); } - internalUnpackBucketCount += - (DocumentSourceInternalUnpackBucket::kStageNameInternal == stageName || - DocumentSourceInternalUnpackBucket::kStageNameExternal == stageName) - ? 1 - : 0; - for (auto&& subPipeline : stage->getSubPipelines()) { subPipeline.validate(opCtx, performApiVersionChecks); } } - - - // Validates that the pipeline contains at most one $_internalUnpackBucket or $_unpackBucket - // stage. - uassert(5348302, - str::stream() << "Encountered pipeline with more than one " - << DocumentSourceInternalUnpackBucket::kStageNameInternal << " or " - << DocumentSourceInternalUnpackBucket::kStageNameExternal << " stage", - internalUnpackBucketCount <= 1); } } // namespace mongo diff --git a/src/mongo/db/pipeline/lite_parsed_pipeline.h b/src/mongo/db/pipeline/lite_parsed_pipeline.h index 712d23c32bd..2c424a84ee3 100644 --- a/src/mongo/db/pipeline/lite_parsed_pipeline.h +++ b/src/mongo/db/pipeline/lite_parsed_pipeline.h @@ -123,6 +123,13 @@ public: } /** + * Returns true if the pipeline begins with a $documents stage. + */ + bool startsWithDocuments() const { + return !_stageSpecs.empty() && _stageSpecs.front()->isDocuments(); + } + + /** * Returns true if the pipeline has a $changeStream stage. */ bool hasChangeStream() const { @@ -132,6 +139,13 @@ public: } /** + * Returns true if the pipeline ends with a $changeStreamSplitLargeEvent stage. + */ + bool endsWithChangeStreamSplitLargeEvent() const { + return !_stageSpecs.empty() && _stageSpecs.back()->isChangeStreamSplitLargeEvent(); + } + + /** * Returns false if the pipeline has any stages which cannot be passed through to the shards. */ bool allowedToPassthroughFromMongos() const { @@ -207,8 +221,7 @@ public: /** * Verifies that the pipeline contains valid stages. Optionally calls - * 'validatePipelineStagesforAPIVersion' with 'opCtx', and throws UserException if there is - * more than one $_internalUnpackBucket stage in the pipeline. + * 'validatePipelineStagesforAPIVersion' with 'opCtx'. */ void validate(const OperationContext* opCtx, bool performApiVersionChecks = true) const; diff --git a/src/mongo/db/pipeline/lookup_set_cache_test.cpp b/src/mongo/db/pipeline/lookup_set_cache_test.cpp index 6670b5b5971..ec78b0b0572 100644 --- a/src/mongo/db/pipeline/lookup_set_cache_test.cpp +++ b/src/mongo/db/pipeline/lookup_set_cache_test.cpp @@ -214,7 +214,7 @@ TEST(LookupSetCacheTest, DocumentWithStorageCachePopulated) { // initialization. BSONObj input = BSON("a" << 1); const auto doc1 = Document(input); - const auto sizeOfDoc1Before = doc1.getApproximateSize(); + const auto sizeOfDoc1Before = doc1.getCurrentApproximateSize(); auto key = Value("foo"_sd); // Insert a cache entry and verify that both the key and the document are accounted for in the @@ -228,14 +228,14 @@ TEST(LookupSetCacheTest, DocumentWithStorageCachePopulated) { auto prevCacheSize = cache.getMemoryUsage(); const auto doc2 = Document({{"a", 2}}); cache.insert(key, doc2); - ASSERT_EQ(cache.getMemoryUsage(), prevCacheSize + doc2.getApproximateSize()); + ASSERT_EQ(cache.getMemoryUsage(), prevCacheSize + doc2.getCurrentApproximateSize()); // Calling serializeForSorter() should grow the overall document size. Verify that growing the // size of the 'Document' object does not have impact on the size stored in 'cache'. prevCacheSize = cache.getMemoryUsage(); BufBuilder builder; doc1.serializeForSorter(builder); - ASSERT_LT(sizeOfDoc1Before, doc1.getApproximateSize()); + ASSERT_LT(sizeOfDoc1Before, doc1.getCurrentApproximateSize()); ASSERT_EQ(prevCacheSize, cache.getMemoryUsage()); cache.evictOne(); diff --git a/src/mongo/db/pipeline/map_reduce_options.idl b/src/mongo/db/pipeline/map_reduce_options.idl new file mode 100644 index 00000000000..55d5e133b6f --- /dev/null +++ b/src/mongo/db/pipeline/map_reduce_options.idl @@ -0,0 +1,42 @@ +# Copyright (C) 2022-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# As a special exception, the copyright holders give permission to link the +# code of portions of this program with the OpenSSL library under certain +# conditions as described in each individual source file and distribute +# linked combinations including the program with the OpenSSL library. You +# must comply with the Server Side Public License in all respects for +# all of the code used other than as permitted herein. If you modify file(s) +# with this exception, you may extend this exception to your version of the +# file(s), but you are not obligated to do so. If you do not wish to do so, +# delete this exception statement from your version. If you delete this +# exception statement from all source files in the program, then also delete +# it in the license file. +# + +global: + cpp_namespace: "mongo" + +server_parameters: + mrEnableSingleReduceOptimization: + description: > + In version 4.2 and before, MongoDB MapReduce will not call the reduce function + for a key that has only a single value. In version 4.4 and later, the reduce + function is still called in order to validate the JavaScript reduce function + even when there is only one value. This setting will re-enable the old optimization. + set_at: startup + cpp_vartype: bool + cpp_varname: mrSingleReduceOptimizationEnabled + default: false diff --git a/src/mongo/db/pipeline/memory_usage_tracker.h b/src/mongo/db/pipeline/memory_usage_tracker.h index 275183081ef..b91f0eb57ac 100644 --- a/src/mongo/db/pipeline/memory_usage_tracker.h +++ b/src/mongo/db/pipeline/memory_usage_tracker.h @@ -50,20 +50,8 @@ public: PerFunctionMemoryTracker() = delete; void update(long long diff) { - - // TODO SERVER-61281: this is a temporary measure in tackling the problem in this - // ticket. It prevents the underflow from happening but doesn't address the cause - // which is inaccurate tracking. - // Once inaccurate tracking is resolved, the underflow assertion below could be - // restored. - // tassert(5578603, - // str::stream() << "Underflow on memory tracking, attempting to add " << - // diff - // << " but only " << _currentMemoryBytes << " available", - // diff >= 0 || _currentMemoryBytes >= std::abs(diff)); - // set(_currentMemoryBytes + diff); - - set(std::max(_currentMemoryBytes + diff, 0LL)); + // TODO SERVER-61281: Check for memory underflow. + set(std::max(_currentMemoryBytes + diff, 0ll)); } void set(long long total) { @@ -154,11 +142,8 @@ public: * Updates total memory usage. */ void update(long long diff) { - tassert(5578602, - str::stream() << "Underflow on memory tracking, attempting to add " << diff - << " but only " << _memoryUsageBytes << " available", - diff >= 0 || (int)_memoryUsageBytes >= -1 * diff); - set(_memoryUsageBytes + diff); + // TODO SERVER-61281: Check for memory underflow. + set(std::max(_memoryUsageBytes + diff, 0ll)); } auto currentMemoryBytes() const { diff --git a/src/mongo/db/pipeline/memory_usage_tracker_test.cpp b/src/mongo/db/pipeline/memory_usage_tracker_test.cpp index cc354e7fd69..f60933a4655 100644 --- a/src/mongo/db/pipeline/memory_usage_tracker_test.cpp +++ b/src/mongo/db/pipeline/memory_usage_tracker_test.cpp @@ -99,28 +99,28 @@ TEST_F(MemoryUsageTrackerTest, UpdateUsageUpdatesGlobal) { ASSERT_EQ(_tracker.maxMemoryBytes(), 150LL); } -DEATH_TEST_F(MemoryUsageTrackerTest, - UpdateGlobalToNegativeIsDisallowed, - "Underflow on memory tracking") { - _tracker.set(50LL); +// TODO SERVER-61281: Switch to 'DEATH_TEST_F' checking the underflow case. +TEST_F(MemoryUsageTrackerTest, UpdateFunctionUsageToNegativeIsDisallowed) { + _funcTracker.set(50LL); + ASSERT_EQ(_funcTracker.currentMemoryBytes(), 50LL); + ASSERT_EQ(_funcTracker.maxMemoryBytes(), 50LL); ASSERT_EQ(_tracker.currentMemoryBytes(), 50LL); ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); - _tracker.update(-100); + _funcTracker.update(-100); + ASSERT_EQ(_tracker.currentMemoryBytes(), 0LL); + ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); } -TEST_F(MemoryUsageTrackerTest, UpdateFunctionUsageToNegativeIsDisallowed) { - _funcTracker.set(50LL); +// TODO SERVER-61281: Switch to 'DEATH_TEST_F' checking the underflow case. +TEST_F(MemoryUsageTrackerTest, UpdateMemUsageToNegativeIsDisallowed) { + _tracker.set(50LL); ASSERT_EQ(_tracker.currentMemoryBytes(), 50LL); ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); - // TODO SERVER-61281: Temporarily disable the assert (and associated test) in - // PerFunctionMemoryTracker.update() to prevent inaccurate tracking to cause underflow errors - // Once accurate tracking is implemented and no underflow should happen, this negative test - // could be restored to verify that "Underflow on memory tracking" is reported. - - _funcTracker.update(-100); + _tracker.update(-100); ASSERT_EQ(_tracker.currentMemoryBytes(), 0LL); + ASSERT_EQ(_tracker.maxMemoryBytes(), 50LL); } } // namespace diff --git a/src/mongo/db/pipeline/monotonic_expression.cpp b/src/mongo/db/pipeline/monotonic_expression.cpp new file mode 100644 index 00000000000..bc34b756fec --- /dev/null +++ b/src/mongo/db/pipeline/monotonic_expression.cpp @@ -0,0 +1,60 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/monotonic_expression.h" + +namespace mongo::monotonic { + +State opposite(State state) { + switch (state) { + case State::NonMonotonic: + case State::Constant: + return state; + case State::Increasing: + return State::Decreasing; + case State::Decreasing: + return State::Increasing; + }; + MONGO_UNREACHABLE; +} + +State combine(State lhs, State rhs) { + if (lhs == State::NonMonotonic || rhs == State::NonMonotonic) { + return State::NonMonotonic; + } + if (lhs == rhs || lhs == State::Constant) { + return rhs; + } + if (rhs == State::Constant) { + return lhs; + } + return State::NonMonotonic; +} + +} // namespace mongo::monotonic diff --git a/src/mongo/db/pipeline/monotonic_expression.h b/src/mongo/db/pipeline/monotonic_expression.h new file mode 100644 index 00000000000..6f1664cf199 --- /dev/null +++ b/src/mongo/db/pipeline/monotonic_expression.h @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include "mongo/db/pipeline/field_path.h" + +namespace mongo::monotonic { + +enum class State { NonMonotonic, Constant, Increasing, Decreasing }; + +/** + * Given monotonic states of function f(x), returns monotonic state of -f(x). If function is + * constant or non monotonic, it will remain the same. If it is increasing, it will become + * decreasing, and vice versa. + */ +State opposite(State state); + +/** + * Given monotonic states of functions f(x) and g(x), returns monotonic state of f(x)+g(x). Plus + * operator can be replaced with any operation that preserves monotonic behavior. + * + * If any argument is non monotonic, then the whole function is non monotonic. + * If one of the arguments is a constant, then the whole function has the same monotonic state as + * the other argument. If all arguments have the the same monotonic state, then the whole function + * has the same monotonic state. Otherwise, the result is NonMonotonic. + */ +State combine(State lhs, State rhs); + +template <typename ExpressionsContainer> +State combineExpressions(const FieldPath& sortedFieldPath, const ExpressionsContainer& container) { + return std::accumulate(container.begin(), + container.end(), + State::Constant, + [&](State state, const auto& expression) { + if (expression == nullptr) { + return state; + } + return combine(state, + expression->getMonotonicState(sortedFieldPath)); + }); +} + +} // namespace mongo::monotonic diff --git a/src/mongo/db/pipeline/monotonic_expression_test.cpp b/src/mongo/db/pipeline/monotonic_expression_test.cpp new file mode 100644 index 00000000000..0c667108201 --- /dev/null +++ b/src/mongo/db/pipeline/monotonic_expression_test.cpp @@ -0,0 +1,149 @@ +/** + * Copyright (C) 2022-present MongoDB, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * <http://www.mongodb.com/licensing/server-side-public-license>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the Server Side Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#include "mongo/db/pipeline/expression_context_for_test.h" +#include "mongo/db/pipeline/monotonic_expression.h" +#include "mongo/db/service_context_d_test_fixture.h" + +namespace mongo { +namespace { + +class MonotonicExpressionFixture : public ServiceContextMongoDTest { +public: + bool isMonotonicExpression(BSONObj expressionSpec, const FieldPath& monotonicField) { + auto expression = + Expression::parseExpression(&_expCtx, expressionSpec, _expCtx.variablesParseState); + return expression->isMonotonic(monotonicField); + } + +private: + ExpressionContextForTest _expCtx; +}; + +} // namespace + +TEST_F(MonotonicExpressionFixture, ConstIsMonotonic) { + ASSERT_TRUE(isMonotonicExpression(BSON("$const" << 1), "a")); +} + +TEST_F(MonotonicExpressionFixture, MonotonicFieldIsMonotonic) { + ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$a")), "a")); +} + +TEST_F(MonotonicExpressionFixture, NonMonotonicFieldIsNonMonotonic) { + ASSERT_FALSE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$b")), "a")); +} + +TEST_F(MonotonicExpressionFixture, MonotonicWithOppositeSignIsStillMonotonic) { + ASSERT_TRUE(isMonotonicExpression(BSON("$subtract" << BSON_ARRAY(1 << "$a")), "a")); +} + +TEST_F(MonotonicExpressionFixture, NestedOppositesAreProcessedCorrectlyForMonotonic) { + // Test expression is: dateDiff(1 - (1 - (1 - a))), a) + const auto& startDate = fromjson("{$subtract: [1, {$subtract: [1, {$subtract: [1, '$a']}]}]}"); + const auto& expression = BSON("$dateDiff" << BSON("startDate" << startDate << "endDate" + << "$a" + << "unit" + << "hours")); + ASSERT_TRUE(isMonotonicExpression(expression, "a")); +} + +TEST_F(MonotonicExpressionFixture, NestedOppositesAreProcessedCorrectlyForNonMonotonic) { + // LHS expression (1 - (1 - floor(a)) - 1 is increasing assuming $a is increasing. + const auto& lhs = + fromjson("{$subtract: [{$subtract: [1, {$subtract: [1, {$floor: '$a'}]}]}, 1]}"); + // RHS expression 1 - (1 - (1 - ceil(a))) is decreasing assuming $a is decreasing. + const auto& rhs = + fromjson("{$subtract: [1, {$subtract: [1, {$subtract: [1, {$ceil: '$a'}]}]}]}"); + // Both LHS and RHS are monotonic + ASSERT_TRUE(isMonotonicExpression(lhs, "a")); + ASSERT_TRUE(isMonotonicExpression(rhs, "a")); + // Because they are monotonic in different directions, their sum is non monotonic, but their + // difference is monotonic. + ASSERT_FALSE(isMonotonicExpression(BSON("$add" << BSON_ARRAY(lhs << rhs)), "a")); + ASSERT_TRUE(isMonotonicExpression(BSON("$subtract" << BSON_ARRAY(lhs << rhs)), "a")); +} + +TEST_F(MonotonicExpressionFixture, MonotonicAndNonMonotonicFieldIsNonMonotonic) { + ASSERT_FALSE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$a" + << "$b")), + "a")); +} + +TEST_F(MonotonicExpressionFixture, MonotonicAndConstantIsMonotonic) { + ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$a" + << "1")), + "a")); +} + +TEST_F(MonotonicExpressionFixture, ConstantAndConstantIsMonotonic) { + ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("1" + << "2")), + "a")); +} + +TEST_F(MonotonicExpressionFixture, MonotonicAndOppositeMonotonicIsNonMonotonic) { + ASSERT_FALSE(isMonotonicExpression(BSON("$subtract" << BSON_ARRAY("$a" + << "$a")), + "a")); +} + +TEST_F(MonotonicExpressionFixture, MonotonicAndMonotonicIsMonotonic) { + const auto& dateTrunc = BSON("$dateTrunc" << BSON("date" + << "$time" + << "unit" + << "hour" + << "timezone" + << "America/New_York")); + ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY(dateTrunc << "$time")), "time")); + ASSERT_TRUE(isMonotonicExpression(BSON("$add" << BSON_ARRAY("$time" + << "$time")), + "time")); +} +TEST_F(MonotonicExpressionFixture, FunctionWithConstantNonMonotonicChildrenIsMonotonic) { + ASSERT_TRUE(isMonotonicExpression(BSON("$dateTrunc" << BSON("date" + << "$time" + << "unit" + << "hour" + << "timezone" + << "America/New_York")), + "time")); +} + +TEST_F(MonotonicExpressionFixture, FunctionWithNonConstantNonMonotonicChildrenIsNonMonotonic) { + ASSERT_FALSE(isMonotonicExpression(BSON("$dateTrunc" << BSON("date" + << "$time" + << "unit" + << "hour" + << "binSize" + << "$time")), + "time")); +} + +} // namespace mongo diff --git a/src/mongo/db/pipeline/pipeline.cpp b/src/mongo/db/pipeline/pipeline.cpp index 97a896e5898..22ca4e7a85f 100644 --- a/src/mongo/db/pipeline/pipeline.cpp +++ b/src/mongo/db/pipeline/pipeline.cpp @@ -40,6 +40,7 @@ #include "mongo/db/jsobj.h" #include "mongo/db/operation_context.h" #include "mongo/db/pipeline/accumulator.h" +#include "mongo/db/pipeline/change_stream_helpers.h" #include "mongo/db/pipeline/document_source.h" #include "mongo/db/pipeline/document_source_match.h" #include "mongo/db/pipeline/document_source_merge.h" @@ -105,14 +106,32 @@ void validateTopLevelPipeline(const Pipeline& pipeline) { // If the first stage is a $changeStream stage, then all stages in the pipeline must be // either $changeStream stages or allowlisted as being able to run in a change stream. - if (firstStageConstraints.isChangeStreamStage()) { - for (auto&& source : sources) { - uassert(ErrorCodes::IllegalOperation, - str::stream() << source->getSourceName() - << " is not permitted in a $changeStream pipeline", - source->constraints().isAllowedInChangeStream()); + const bool isChangeStream = firstStageConstraints.isChangeStreamStage(); + // Record whether any of the stages in the pipeline is a $changeStreamSplitLargeEvent. + bool hasChangeStreamSplitLargeEventStage = false; + for (auto&& source : sources) { + uassert(ErrorCodes::IllegalOperation, + str::stream() << source->getSourceName() + << " is not permitted in a $changeStream pipeline", + !(isChangeStream && !source->constraints().isAllowedInChangeStream())); + // Check whether any stages must only be run in a change stream pipeline. + uassert(ErrorCodes::IllegalOperation, + str::stream() << source->getSourceName() + << " can only be used in a $changeStream pipeline", + !(source->constraints().requiresChangeStream() && !isChangeStream)); + // Check whether this is a change stream split stage. + if ("$changeStreamSplitLargeEvent"_sd == source->getSourceName()) { + hasChangeStreamSplitLargeEventStage = true; } } + auto expCtx = pipeline.getContext(); + auto spec = isChangeStream ? expCtx->changeStreamSpec : boost::none; + auto hasSplitEventResumeToken = spec && + change_stream::resolveResumeTokenFromSpec(expCtx, *spec).fragmentNum.has_value(); + uassert(ErrorCodes::ChangeStreamFatalError, + "To resume from a split event, the $changeStream pipeline must include a " + "$changeStreamSplitLargeEvent stage", + !(hasSplitEventResumeToken && !hasChangeStreamSplitLargeEventStage)); } // Verify that usage of $searchMeta and $search is legal. @@ -231,39 +250,41 @@ std::unique_ptr<Pipeline, PipelineDeleter> Pipeline::create( } void Pipeline::validateCommon(bool alreadyOptimized) const { - size_t i = 0; - uassert(ErrorCodes::FailedToParse, str::stream() << "Pipeline length must be no longer than " << internalPipelineLengthLimit << " stages", static_cast<int>(_sources.size()) <= internalPipelineLengthLimit); - for (auto&& stage : _sources) { + // Keep track of stages which can only appear once. + std::set<StringData> singleUseStages; + + for (auto sourceIter = _sources.begin(); sourceIter != _sources.end(); ++sourceIter) { + auto& stage = *sourceIter; auto constraints = stage->constraints(_splitState); // Verify that all stages adhere to their PositionRequirement constraints. uassert(40602, str::stream() << stage->getSourceName() << " is only valid as the first stage in a pipeline", - !(constraints.requiredPosition == PositionRequirement::kFirst && i != 0)); - uassert(40603, - str::stream() << stage->getSourceName() - << " is only valid as the first stage in an optimized pipeline", - !(alreadyOptimized && - constraints.requiredPosition == PositionRequirement::kFirstAfterOptimization && - i != 0)); + !(constraints.requiredPosition == PositionRequirement::kFirst && + sourceIter != _sources.begin())); + // TODO SERVER-73790: use PositionRequirement::kCustom to validate $match. auto matchStage = dynamic_cast<DocumentSourceMatch*>(stage.get()); uassert(17313, "$match with $text is only allowed as the first pipeline stage", - !(i != 0 && matchStage && matchStage->isTextQuery())); + !(sourceIter != _sources.begin() && matchStage && matchStage->isTextQuery())); uassert(40601, str::stream() << stage->getSourceName() << " can only be the final stage in the pipeline", !(constraints.requiredPosition == PositionRequirement::kLast && - i != _sources.size() - 1)); - ++i; + std::next(sourceIter) != _sources.end())); + + // If the stage has a special requirement about its position, validate it. + if (constraints.requiredPosition == PositionRequirement::kCustom) { + stage->validatePipelinePosition(alreadyOptimized, sourceIter, _sources); + } // Verify that we are not attempting to run a mongoS-only stage on mongoD. uassert(40644, @@ -275,6 +296,12 @@ void Pipeline::validateCommon(bool alreadyOptimized) const { str::stream() << "Stage not supported inside of a multi-document transaction: " << stage->getSourceName(), !(pCtx->opCtx->inMultiDocumentTransaction() && !constraints.isAllowedInTransaction())); + + // Verify that a stage which can only appear once doesn't appear more than that. + uassert(7183900, + str::stream() << stage->getSourceName() << " can only be used once in the pipeline", + !(constraints.canAppearOnlyOnceInPipeline && + !singleUseStages.insert(stage->getSourceName()).second)); } } @@ -721,6 +748,24 @@ Pipeline::SourceContainer::iterator Pipeline::optimizeEndOfPipeline( return std::next(itr); } +Pipeline::SourceContainer::iterator Pipeline::optimizeAtEndOfPipeline( + Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { + if (itr == container->end()) { + return itr; + } + itr = std::next(itr); + try { + while (itr != container->end()) { + invariant((*itr).get()); + itr = (*itr).get()->optimizeAt(itr, container); + } + } catch (DBException& ex) { + ex.addContext("Failed to optimize pipeline"); + throw; + } + return itr; +} + std::unique_ptr<Pipeline, PipelineDeleter> Pipeline::makePipelineFromViewDefinition( const boost::intrusive_ptr<ExpressionContext>& subPipelineExpCtx, ExpressionContext::ResolvedNamespace resolvedNs, diff --git a/src/mongo/db/pipeline/pipeline.h b/src/mongo/db/pipeline/pipeline.h index 96cf6426be3..8965f493dc2 100644 --- a/src/mongo/db/pipeline/pipeline.h +++ b/src/mongo/db/pipeline/pipeline.h @@ -171,6 +171,16 @@ public: static Pipeline::SourceContainer::iterator optimizeEndOfPipeline( Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container); + /** + * Applies optimizeAt() to all stages in the given pipeline after the stage that 'itr' points + * to. + * + * Returns a valid iterator that points to the new "end of the pipeline": i.e., the stage that + * comes after 'itr' in the newly optimized pipeline. + */ + static Pipeline::SourceContainer::iterator optimizeAtEndOfPipeline( + Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container); + static std::unique_ptr<Pipeline, PipelineDeleter> makePipelineFromViewDefinition( const boost::intrusive_ptr<ExpressionContext>& subPipelineExpCtx, ExpressionContext::ResolvedNamespace resolvedNs, diff --git a/src/mongo/db/pipeline/pipeline_d.cpp b/src/mongo/db/pipeline/pipeline_d.cpp index b21e8635c51..7d2eef919f4 100644 --- a/src/mongo/db/pipeline/pipeline_d.cpp +++ b/src/mongo/db/pipeline/pipeline_d.cpp @@ -42,7 +42,6 @@ #include "mongo/db/catalog/database.h" #include "mongo/db/catalog/index_catalog.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/exec/cached_plan.h" #include "mongo/db/exec/collection_scan.h" @@ -802,10 +801,10 @@ namespace { * the case of a $sort with a non-null value for getLimitSrc(), indicating that there was previously * a $limit stage that was optimized away. */ -std::pair<boost::intrusive_ptr<DocumentSourceSort>, boost::intrusive_ptr<DocumentSourceGroup>> +std::pair<boost::intrusive_ptr<DocumentSourceSort>, boost::intrusive_ptr<DocumentSourceGroupBase>> getSortAndGroupStagesFromPipeline(const Pipeline::SourceContainer& sources) { boost::intrusive_ptr<DocumentSourceSort> sortStage = nullptr; - boost::intrusive_ptr<DocumentSourceGroup> groupStage = nullptr; + boost::intrusive_ptr<DocumentSourceGroupBase> groupStage = nullptr; auto sourcesIt = sources.begin(); if (sourcesIt != sources.end()) { @@ -821,7 +820,7 @@ getSortAndGroupStagesFromPipeline(const Pipeline::SourceContainer& sources) { } if (sourcesIt != sources.end()) { - groupStage = dynamic_cast<DocumentSourceGroup*>(sourcesIt->get()); + groupStage = dynamic_cast<DocumentSourceGroupBase*>(sourcesIt->get()); } return std::make_pair(sortStage, groupStage); @@ -929,8 +928,10 @@ PipelineD::supportsSort(const BucketUnpacker& bucketUnpacker, const CollectionScan* scan = static_cast<CollectionScan*>(root); if (sort.size() == 1) { auto part = sort[0]; - // Check the sort we're asking for is on time. - if (part.fieldPath && *part.fieldPath == bucketUnpacker.getTimeField()) { + // Check the sort we're asking for is on time, and that the buckets are actually + // ordered on time. + if (part.fieldPath && *part.fieldPath == bucketUnpacker.getTimeField() && + !bucketUnpacker.bucketSpec().usesExtendedRange()) { // Check that the directions agree. if ((scan->getDirection() == CollectionScanParams::Direction::FORWARD) == part.isAscending) @@ -1037,6 +1038,15 @@ PipelineD::supportsSort(const BucketUnpacker& bucketUnpacker, if (ixField != controlMinTime && ixField != controlMaxTime) return boost::none; + // If we've inserted a date before 1-1-1970, we round the min up towards 1970, + // rather then down, which has the effect of increasing the control.min.t. + // This means the minimum time in the bucket is likely to be lower than + // indicated and thus, actual dates may be out of order relative to what's + // indicated by the bucket bounds. + if (ixField == controlMinTime && + bucketUnpacker.bucketSpec().usesExtendedRange()) + return boost::none; + if (!directionCompatible(*keyPatternIter, *sortIter)) return boost::none; @@ -1208,12 +1218,7 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll // sort optimization. We check eligibility and perform the rewrite here. auto [unpack, sort] = findUnpackThenSort(pipeline->_sources); QueryPlannerParams plannerOpts; - if (serverGlobalParams.featureCompatibility.isVersionInitialized() && - serverGlobalParams.featureCompatibility.isGreaterThanOrEqualTo( - multiversion::FeatureCompatibilityVersion::kVersion_6_0) && - feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabled( - serverGlobalParams.featureCompatibility) && - unpack && sort) { + if (feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV() && unpack && sort) { plannerOpts.traversalPreference = createTimeSeriesTraversalPreference(unpack, sort); } @@ -1235,12 +1240,7 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll // If this is a query on a time-series collection then it may be eligible for a post-planning // sort optimization. We check eligibility and perform the rewrite here. - if (serverGlobalParams.featureCompatibility.isVersionInitialized() && - serverGlobalParams.featureCompatibility.isGreaterThanOrEqualTo( - multiversion::FeatureCompatibilityVersion::kVersion_6_0) && - feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabled( - serverGlobalParams.featureCompatibility) && - unpack && sort) { + if (feature_flags::gFeatureFlagBucketUnpackWithSort.isEnabledAndIgnoreFCV() && unpack && sort) { auto execImpl = dynamic_cast<PlanExecutorImpl*>(exec.get()); if (execImpl) { @@ -1306,7 +1306,7 @@ PipelineD::buildInnerQueryExecutorGeneric(const MultipleCollectionAccessor& coll dynamic_cast<const DocumentSourceInternalUnpackBucket*>( iter->get())) { unpackIter = iter; - uassert(6505001, + tassert(6505001, str::stream() << "Expected at most one " << DocumentSourceInternalUnpackBucket::kStageNameInternal @@ -1669,7 +1669,7 @@ StatusWith<std::unique_ptr<PlanExecutor, PlanExecutor::Deleter>> PipelineD::prep // will handle the sort, and the groupTransform (added below) will handle the $group // stage. pipeline->popFrontWithName(DocumentSourceSort::kStageName); - pipeline->popFrontWithName(DocumentSourceGroup::kStageName); + pipeline->popFrontWithName(rewrittenGroupStage->originalStageName()); boost::intrusive_ptr<DocumentSource> groupTransform( new DocumentSourceSingleDocumentTransformation( diff --git a/src/mongo/db/pipeline/pipeline_test.cpp b/src/mongo/db/pipeline/pipeline_test.cpp index 1854c07a97a..cd1b99870bc 100644 --- a/src/mongo/db/pipeline/pipeline_test.cpp +++ b/src/mongo/db/pipeline/pipeline_test.cpp @@ -75,6 +75,7 @@ using std::string; using std::vector; const NamespaceString kTestNss = NamespaceString("a.collection"); +const NamespaceString kAdminCollectionlessNss = NamespaceString("admin.$cmd.aggregate"); constexpr size_t getChangeStreamStageSize() { return 6; @@ -114,7 +115,8 @@ class StubExplainInterface : public StubMongoProcessInterface { }; void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson, std::string outputPipeJson, - std::string serializedPipeJson) { + std::string serializedPipeJson, + NamespaceString aggNss = kTestNss) { QueryTestServiceContext testServiceContext; auto opCtx = testServiceContext.makeOperationContext(); @@ -128,7 +130,7 @@ void assertPipelineOptimizesAndSerializesTo(std::string inputPipeJson, ASSERT_EQUALS(stageElem.type(), BSONType::Object); rawPipeline.push_back(stageElem.embeddedObject()); } - AggregateCommandRequest request(kTestNss, rawPipeline); + AggregateCommandRequest request(aggNss, rawPipeline); intrusive_ptr<ExpressionContextForTest> ctx = new ExpressionContextForTest(opCtx.get(), request); ctx->mongoProcessInterface = std::make_shared<StubExplainInterface>(); @@ -2981,6 +2983,71 @@ TEST(PipelineOptimizationTest, MatchGetsPushedIntoBothChildrenOfUnion) { "]"); } +TEST(PipelineOptimizationTest, internalAllCollectionStatsAbsorbsMatchOnNs) { + std::string inputPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {ns: 'test.foo', a: 10}}" + "]"; + std::string outputPipe = + "[" + " {$_internalAllCollectionStats: {match: {ns: {$eq: 'test.foo'}}}}," + " {$match: {a: {$eq: 10}}}" + "]"; + std::string serializedPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {ns: {$eq: 'test.foo'}}}," + " {$match: {a: {$eq: 10}}}" + "]"; + assertPipelineOptimizesAndSerializesTo( + inputPipe, outputPipe, serializedPipe, kAdminCollectionlessNss); +} + +TEST(PipelineOptimizationTest, internalAllCollectionStatsAbsorbsSeveralMatchesOnNs) { + std::string inputPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {ns: {$gt: 0}}}," + " {$match: {a: 10}}," + " {$match: {ns: {$ne: 5}}}" + "]"; + std::string outputPipe = + "[" + " {$_internalAllCollectionStats: {match: {$and: [{ns: {$gt: 0}}, {ns: {$not: {$eq: " + "5}}}]}}}," + " {$match: {a: {$eq: 10}}}" + "]"; + std::string serializedPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {$and: [{ns: {$gt: 0}}, {ns: {$not: {$eq: 5}}}]}}," + " {$match: {a: {$eq: 10}}}" + "]"; + assertPipelineOptimizesAndSerializesTo( + inputPipe, outputPipe, serializedPipe, kAdminCollectionlessNss); +} + +TEST(PipelineOptimizationTest, internalAllCollectionStatsDoesNotAbsorbMatchNotOnNs) { + std::string inputPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {a: 10}}" + "]"; + std::string outputPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {a: {$eq: 10}}}" + "]"; + std::string serializedPipe = + "[" + " {$_internalAllCollectionStats: {}}," + " {$match: {a: 10}}" + "]"; + assertPipelineOptimizesAndSerializesTo( + inputPipe, outputPipe, serializedPipe, kAdminCollectionlessNss); +} + TEST(PipelineOptimizationTest, ProjectGetsPushedIntoBothChildrenOfUnion) { assertPipelineOptimizesTo( "[" @@ -4599,7 +4666,7 @@ public: * Returns a description which communicate that this stage modifies nothing. */ GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>(), {}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet(), {}}; } }; @@ -4676,7 +4743,7 @@ public: * Returns a description which communicate that this stage modifies nothing. */ GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>(), {}}; + return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet(), {}}; } }; @@ -4712,7 +4779,7 @@ public: return new RenamesAToB(expCtx); } GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"b", "a"}}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"b", "a"}}}; } }; @@ -4836,7 +4903,7 @@ public: return new RenamesBToC(expCtx); } GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"c", "b"}}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"c", "b"}}}; } }; @@ -4879,7 +4946,7 @@ public: return new RenamesBToA(expCtx); } GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"a", "b"}}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"a", "b"}}}; } }; diff --git a/src/mongo/db/pipeline/plan_executor_pipeline.cpp b/src/mongo/db/pipeline/plan_executor_pipeline.cpp index 0b26a7db813..958c8a653af 100644 --- a/src/mongo/db/pipeline/plan_executor_pipeline.cpp +++ b/src/mongo/db/pipeline/plan_executor_pipeline.cpp @@ -39,6 +39,11 @@ #include "mongo/db/repl/speculative_majority_read_info.h" namespace mongo { +namespace { +Counter64 changeStreamsLargeEventsFailedCounter; +ServerStatusMetricField<Counter64> dChangeStreamsLargeEventsFailedCounter( + "changeStreams.largeEventsFailed", &changeStreamsLargeEventsFailedCounter); +} // namespace PlanExecutorPipeline::PlanExecutorPipeline(boost::intrusive_ptr<ExpressionContext> expCtx, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, @@ -78,9 +83,7 @@ PlanExecutor::ExecState PlanExecutorPipeline::getNext(BSONObj* objOut, RecordId* Document docOut; auto execState = getNextDocument(&docOut, nullptr); if (execState == PlanExecutor::ADVANCED) { - // Include metadata if the output will be consumed by a merging node. - *objOut = _expCtx->needsMerge || _expCtx->forPerShardCursor ? docOut.toBsonWithMetaData() - : docOut.toBson(); + *objOut = _trySerializeToBson(docOut); } return execState; } @@ -140,6 +143,19 @@ boost::optional<Document> PlanExecutorPipeline::_tryGetNext() try { return Document::fromBsonWithMetaData(extraInfo->getStartAfterInvalidateEvent()); } +BSONObj PlanExecutorPipeline::_trySerializeToBson(const Document& doc) try { + // Include metadata if the output will be consumed by a merging node. + return _expCtx->needsMerge || _expCtx->forPerShardCursor ? doc.toBsonWithMetaData() + : doc.toBson(); +} catch (const ExceptionFor<ErrorCodes::BSONObjectTooLarge>&) { + // If in a change stream pipeline, increment change stream large event failed error + // count metric. + if (ResumableScanType::kChangeStream == _resumableScanType) { + changeStreamsLargeEventsFailedCounter.increment(); + } + throw; +} + void PlanExecutorPipeline::_updateResumableScanState(const boost::optional<Document>& document) { switch (_resumableScanType) { case ResumableScanType::kChangeStream: @@ -185,9 +201,8 @@ void PlanExecutorPipeline::_performChangeStreamsAccounting(const boost::optional void PlanExecutorPipeline::_validateChangeStreamsResumeToken(const Document& event) const { // Confirm that the document _id field matches the original resume token in the sort key field. - auto eventBSON = event.toBson(); auto resumeToken = event.metadata().getSortKey(); - auto idField = eventBSON.getObjectField("_id"); + auto idField = event.getField("_id"); invariant(!resumeToken.missing()); uassert(ErrorCodes::ChangeStreamFatalError, str::stream() << "Encountered an event whose _id field, which contains the resume " @@ -196,9 +211,9 @@ void PlanExecutorPipeline::_validateChangeStreamsResumeToken(const Document& eve "transformations that retain the unmodified _id field are allowed. " "Expected: " << BSON("_id" << resumeToken) << " but found: " - << (eventBSON["_id"] ? BSON("_id" << eventBSON["_id"]) : BSONObj()), - (resumeToken.getType() == BSONType::Object) && - idField.binaryEqual(resumeToken.getDocument().toBson())); + << (idField.missing() ? BSONObj() : BSON("_id" << idField)), + resumeToken.getType() == BSONType::Object && + ValueComparator::kInstance.evaluate(idField == resumeToken)); } void PlanExecutorPipeline::_performResumableOplogScanAccounting() { diff --git a/src/mongo/db/pipeline/plan_executor_pipeline.h b/src/mongo/db/pipeline/plan_executor_pipeline.h index 7192de3668f..52139d1b34d 100644 --- a/src/mongo/db/pipeline/plan_executor_pipeline.h +++ b/src/mongo/db/pipeline/plan_executor_pipeline.h @@ -182,6 +182,11 @@ private: boost::optional<Document> _tryGetNext(); /** + * Serialize the given document to BSON while updating stats for BSONObjectTooLarge exception. + */ + BSONObj _trySerializeToBson(const Document& doc); + + /** * For a change stream or resumable oplog scan, updates the scan state based on the latest * document returned by the underlying pipeline. */ diff --git a/src/mongo/db/pipeline/process_interface/SConscript b/src/mongo/db/pipeline/process_interface/SConscript index 44b0afe1591..c9d3c820073 100644 --- a/src/mongo/db/pipeline/process_interface/SConscript +++ b/src/mongo/db/pipeline/process_interface/SConscript @@ -47,6 +47,7 @@ env.Library( '$BUILD_DIR/mongo/db/catalog/catalog_helpers', '$BUILD_DIR/mongo/db/catalog/database_holder', '$BUILD_DIR/mongo/db/collection_index_usage_tracker', + '$BUILD_DIR/mongo/db/concurrency/exception_util', '$BUILD_DIR/mongo/db/concurrency/flow_control_ticketholder', '$BUILD_DIR/mongo/db/dbhelpers', '$BUILD_DIR/mongo/db/index_builds_coordinator_mongod', diff --git a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp index 469ce5821aa..34497978daa 100644 --- a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.cpp @@ -45,7 +45,7 @@ #include "mongo/db/catalog/list_indexes.h" #include "mongo/db/catalog/rename_collection.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/concurrency/exception_util.h" #include "mongo/db/curop.h" #include "mongo/db/cursor_manager.h" #include "mongo/db/db_raii.h" @@ -200,7 +200,8 @@ std::vector<Document> CommonMongodProcessInterface::getIndexStats(OperationConte auto idxCatalog = collection->getIndexCatalog(); auto idx = idxCatalog->findIndexByName(opCtx, indexName, - /* includeUnfinishedIndexes */ true); + IndexCatalog::InclusionPolicy::kReady | + IndexCatalog::InclusionPolicy::kUnfinished); uassert(ErrorCodes::IndexNotFound, "Could not find entry in IndexCatalog for index " + indexName, idx); @@ -604,7 +605,8 @@ bool CommonMongodProcessInterface::fieldsHaveSupportingUniqueIndex( return fieldPaths == std::set<FieldPath>{"_id"}; } - auto indexIterator = collection->getIndexCatalog()->getIndexIterator(opCtx, false); + auto indexIterator = collection->getIndexCatalog()->getIndexIterator( + opCtx, IndexCatalog::InclusionPolicy::kReady); while (indexIterator->more()) { const IndexCatalogEntry* entry = indexIterator->next(); if (supportsUniqueKey(expCtx, entry, fieldPaths)) { @@ -745,59 +747,6 @@ CommonMongodProcessInterface::ensureFieldsUniqueOrResolveDocumentKey( return {*fieldPaths, targetCollectionVersion}; } -write_ops::InsertCommandRequest CommonMongodProcessInterface::buildInsertOp( - const NamespaceString& nss, std::vector<BSONObj>&& objs, bool bypassDocValidation) { - write_ops::InsertCommandRequest insertOp(nss); - insertOp.setDocuments(std::move(objs)); - insertOp.setWriteCommandRequestBase([&] { - write_ops::WriteCommandRequestBase wcb; - wcb.setOrdered(false); - wcb.setBypassDocumentValidation(bypassDocValidation); - return wcb; - }()); - return insertOp; -} - -write_ops::UpdateCommandRequest CommonMongodProcessInterface::buildUpdateOp( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& nss, - BatchedObjects&& batch, - UpsertType upsert, - bool multi) { - write_ops::UpdateCommandRequest updateOp(nss); - updateOp.setUpdates([&] { - std::vector<write_ops::UpdateOpEntry> updateEntries; - for (auto&& obj : batch) { - updateEntries.push_back([&] { - write_ops::UpdateOpEntry entry; - auto&& [q, u, c] = obj; - entry.setQ(std::move(q)); - entry.setU(std::move(u)); - entry.setC(std::move(c)); - entry.setUpsert(upsert != UpsertType::kNone); - entry.setUpsertSupplied( - {{entry.getUpsert(), upsert == UpsertType::kInsertSuppliedDoc}}); - entry.setMulti(multi); - return entry; - }()); - } - return updateEntries; - }()); - updateOp.setWriteCommandRequestBase([&] { - write_ops::WriteCommandRequestBase wcb; - wcb.setOrdered(false); - wcb.setBypassDocumentValidation(expCtx->bypassDocumentValidation); - return wcb; - }()); - auto [constants, letParams] = - expCtx->variablesParseState.transitionalCompatibilitySerialize(expCtx->variables); - updateOp.setLegacyRuntimeConstants(std::move(constants)); - if (!letParams.isEmpty()) { - updateOp.setLet(std::move(letParams)); - } - return updateOp; -} - BSONObj CommonMongodProcessInterface::_convertRenameToInternalRename( OperationContext* opCtx, const BSONObj& renameCommandObj, diff --git a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h index 4a02ecce883..f979430de5f 100644 --- a/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/common_mongod_process_interface.h @@ -142,23 +142,6 @@ protected: const Document& documentKey, MakePipelineOptions opts); - /** - * Builds an ordered insert op on namespace 'nss' and documents to be written 'objs'. - */ - write_ops::InsertCommandRequest buildInsertOp(const NamespaceString& nss, - std::vector<BSONObj>&& objs, - bool bypassDocValidation); - - /** - * Builds an ordered update op on namespace 'nss' with update entries contained in 'batch'. - */ - write_ops::UpdateCommandRequest buildUpdateOp( - const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& nss, - BatchedObjects&& batch, - UpsertType upsert, - bool multi); - BSONObj _reportCurrentOpForClient(OperationContext* opCtx, Client* client, CurrentOpTruncateMode truncateOps, diff --git a/src/mongo/db/pipeline/process_interface/common_process_interface.h b/src/mongo/db/pipeline/process_interface/common_process_interface.h index 513edd5a6f4..55dc54837b1 100644 --- a/src/mongo/db/pipeline/process_interface/common_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/common_process_interface.h @@ -32,6 +32,7 @@ #include <vector> #include "mongo/bson/bsonobj.h" +#include "mongo/db/pipeline/expression_context.h" #include "mongo/db/pipeline/process_interface/mongo_process_interface.h" namespace mongo { @@ -46,6 +47,70 @@ public: virtual ~CommonProcessInterface() = default; /** + * Estimates the size of writes that will be executed on the current node. Note that this + * does not account for the full size of an update statement because in the case of local + * writes, we will not have to serialize to BSON and are therefore not subject to the 16MB + * BSONObj size limit. + */ + class LocalWriteSizeEstimator final : public WriteSizeEstimator { + public: + int estimateInsertHeaderSize( + const write_ops::InsertCommandRequest& insertReq) const override { + return 0; + } + + int estimateUpdateHeaderSize( + const write_ops::UpdateCommandRequest& insertReq) const override { + return 0; + } + + int estimateInsertSizeBytes(const BSONObj& insert) const override { + return insert.objsize(); + } + + int estimateUpdateSizeBytes(const BatchObject& batchObject, + UpsertType type) const override { + int size = std::get<write_ops::UpdateModification>(batchObject).objsize(); + if (auto vars = std::get<boost::optional<BSONObj>>(batchObject)) { + size += vars->objsize(); + } + return size; + } + }; + + /** + * Estimate the size of writes that will be sent to the replica set primary. + */ + class TargetPrimaryWriteSizeEstimator final : public WriteSizeEstimator { + public: + int estimateInsertHeaderSize( + const write_ops::InsertCommandRequest& insertReq) const override { + return write_ops::getInsertHeaderSizeEstimate(insertReq); + } + + int estimateUpdateHeaderSize( + const write_ops::UpdateCommandRequest& updateReq) const override { + return write_ops::getUpdateHeaderSizeEstimate(updateReq); + } + + int estimateInsertSizeBytes(const BSONObj& insert) const override { + return insert.objsize() + write_ops::kWriteCommandBSONArrayPerElementOverheadBytes; + } + + int estimateUpdateSizeBytes(const BatchObject& batchObject, + UpsertType type) const override { + return getUpdateSizeEstimate(std::get<BSONObj>(batchObject), + std::get<write_ops::UpdateModification>(batchObject), + std::get<boost::optional<BSONObj>>(batchObject), + type != UpsertType::kNone /* includeUpsertSupplied */, + boost::none /* collation */, + boost::none /* arrayFilters */, + BSONObj() /* hint*/) + + write_ops::kWriteCommandBSONArrayPerElementOverheadBytes; + } + }; + + /** * Returns true if the field names of 'keyPattern' are exactly those in 'uniqueKeyPaths', and * each of the elements of 'keyPattern' is numeric, i.e. not "text", "$**", or any other special * type of index. @@ -64,6 +129,7 @@ public: virtual std::vector<FieldPath> collectDocumentKeyFieldsActingAsRouter( OperationContext*, const NamespaceString&) const override; + virtual void updateClientOperationTime(OperationContext* opCtx) const final; boost::optional<ChunkVersion> refreshAndGetCollectionVersion( diff --git a/src/mongo/db/pipeline/process_interface/mongo_process_interface.h b/src/mongo/db/pipeline/process_interface/mongo_process_interface.h index 19477adf8c9..5ee0e401924 100644 --- a/src/mongo/db/pipeline/process_interface/mongo_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/mongo_process_interface.h @@ -78,7 +78,7 @@ class MongoProcessInterface { public: /** * Storage for a batch of BSON Objects to be updated in the write namespace. For each element - * in the batch we store a tuple of the folliwng elements: + * in the batch we store a tuple of the following elements: * 1. BSONObj - specifies the query that identifies a document in the to collection to be * updated. * 2. write_ops::UpdateModification - either the new document we want to upsert or insert into @@ -106,6 +106,30 @@ public: enum class CurrentOpBacktraceMode { kIncludeBacktrace, kExcludeBacktrace }; /** + * Interface which estimates the size of a given write operation. + */ + class WriteSizeEstimator { + public: + virtual ~WriteSizeEstimator() = default; + + /** + * Set of functions which estimate the entire size of a write command except for the array + * of write statements themselves. + */ + virtual int estimateInsertHeaderSize( + const write_ops::InsertCommandRequest& insertReq) const = 0; + virtual int estimateUpdateHeaderSize( + const write_ops::UpdateCommandRequest& updateReq) const = 0; + + /** + * Set of functions which estimate the size of a single write statement. + */ + virtual int estimateInsertSizeBytes(const BSONObj& insert) const = 0; + virtual int estimateUpdateSizeBytes(const BatchObject& batchObject, + UpsertType type) const = 0; + }; + + /** * Factory function to create MongoProcessInterface of the right type. The implementation will * be installed by a lib higher up in the link graph depending on the application type. */ @@ -127,6 +151,12 @@ public: virtual ~MongoProcessInterface(){}; /** + * Returns an instance of a 'WriteSizeEstimator' interface. + */ + virtual std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( + OperationContext* opCtx, const NamespaceString& ns) const = 0; + + /** * Creates a new TransactionHistoryIterator object. Only applicable in processes which support * locally traversing the oplog. */ @@ -149,29 +179,30 @@ public: virtual void updateClientOperationTime(OperationContext* opCtx) const = 0; /** - * Inserts 'objs' into 'ns' and returns an error Status if the insert fails. If 'targetEpoch' is - * set, throws ErrorCodes::StaleEpoch if the targeted collection does not have the same epoch or - * the epoch changes during the course of the insert. + * Executes 'insertCommand' against 'ns' and returns an error Status if the insert fails. If + * 'targetEpoch' is set, throws ErrorCodes::StaleEpoch if the targeted collection does not have + * the same epoch or the epoch changes during the course of the insert. */ virtual Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::vector<BSONObj>&& objs, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) = 0; /** - * Updates the documents matching 'queries' with the objects 'updates'. Returns an error Status - * if any of the updates fail, otherwise returns an 'UpdateResult' objects with the details of - * the update operation. If 'targetEpoch' is set, throws ErrorCodes::StaleEpoch if the targeted - * collection does not have the same epoch, or if the epoch changes during the update. + * Executes the updates described by 'updateCommand'. Returns an error Status if any of the + * updates fail, otherwise returns an 'UpdateResult' objects with the details of the update + * operation. If 'targetEpoch' is set, throws ErrorCodes::StaleEpoch if the targeted collection + * does not have the same epoch, or if the epoch changes during the update. */ - virtual StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - BatchedObjects&& batch, - const WriteConcernOptions& wc, - UpsertType upsert, - bool multi, - boost::optional<OID> targetEpoch) = 0; + virtual StatusWith<UpdateResult> update( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, + const WriteConcernOptions& wc, + UpsertType upsert, + bool multi, + boost::optional<OID> targetEpoch) = 0; /** * Returns index usage statistics for each index on collection 'ns' along with additional diff --git a/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp b/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp index 6f17c7a0121..fb0890f5d61 100644 --- a/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/mongos_process_interface.cpp @@ -99,6 +99,12 @@ bool supportsUniqueKey(const boost::intrusive_ptr<ExpressionContext>& expCtx, } // namespace +std::unique_ptr<MongoProcessInterface::WriteSizeEstimator> +MongosProcessInterface::getWriteSizeEstimator(OperationContext* opCtx, + const NamespaceString& ns) const { + return std::make_unique<TargetPrimaryWriteSizeEstimator>(); +} + std::unique_ptr<Pipeline, PipelineDeleter> MongosProcessInterface::attachCursorSourceToPipeline( Pipeline* ownedPipeline, ShardTargetingPolicy shardTargetingPolicy, @@ -175,8 +181,15 @@ boost::optional<Document> MongosProcessInterface::lookupSingleDocument( // single shard will be targeted here; however, in certain cases where only the _id // is present, we may need to scatter-gather the query to all shards in order to // find the document. - auto requests = getVersionedRequestsForTargetedShards( - expCtx->opCtx, nss, cm, findCmd, filterObj, CollationSpec::kSimpleSpec); + auto requests = + getVersionedRequestsForTargetedShards(expCtx->opCtx, + nss, + cm, + findCmd, + filterObj, + CollationSpec::kSimpleSpec, + boost::none /*letParameters*/, + boost::none /*runtimeConstants*/); // Dispatch the requests. The 'establishCursors' method conveniently prepares the // result into a vector of cursor responses for us. diff --git a/src/mongo/db/pipeline/process_interface/mongos_process_interface.h b/src/mongo/db/pipeline/process_interface/mongos_process_interface.h index 82eedfa6dec..28d056b7aa9 100644 --- a/src/mongo/db/pipeline/process_interface/mongos_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/mongos_process_interface.h @@ -45,6 +45,9 @@ public: virtual ~MongosProcessInterface() = default; + std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( + OperationContext* opCtx, const NamespaceString& ns) const final; + boost::optional<Document> lookupSingleDocument( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& nss, @@ -69,7 +72,7 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::vector<BSONObj>&& objs, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, const WriteConcernOptions& wc, boost::optional<OID>) final { MONGO_UNREACHABLE; @@ -77,7 +80,7 @@ public: StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp index 682c0075340..d6d33e12fe7 100644 --- a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.cpp @@ -36,7 +36,7 @@ #include "mongo/db/catalog/list_indexes.h" #include "mongo/db/catalog/rename_collection.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/concurrency/exception_util.h" #include "mongo/db/db_raii.h" #include "mongo/db/index_builds_coordinator.h" #include "mongo/db/pipeline/document_source_cursor.h" @@ -96,13 +96,13 @@ boost::optional<Document> NonShardServerProcessInterface::lookupSingleDocument( return lookedUpDocument; } -Status NonShardServerProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::vector<BSONObj>&& objs, - const WriteConcernOptions& wc, - boost::optional<OID> targetEpoch) { - auto writeResults = write_ops_exec::performInserts( - expCtx->opCtx, buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation)); +Status NonShardServerProcessInterface::insert( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + const WriteConcernOptions& wc, + boost::optional<OID> targetEpoch) { + auto writeResults = write_ops_exec::performInserts(expCtx->opCtx, *insertCommand); // Need to check each result in the batch since the writes are unordered. for (const auto& result : writeResults.results) { @@ -116,13 +116,12 @@ Status NonShardServerProcessInterface::insert(const boost::intrusive_ptr<Express StatusWith<MongoProcessInterface::UpdateResult> NonShardServerProcessInterface::update( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, boost::optional<OID> targetEpoch) { - auto writeResults = write_ops_exec::performUpdates( - expCtx->opCtx, buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi)); + auto writeResults = write_ops_exec::performUpdates(expCtx->opCtx, *updateCommand); // Need to check each result in the batch since the writes are unordered. UpdateResult updateResult; diff --git a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h index ccbe90205c9..9b96e83a1a8 100644 --- a/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/non_shardsvr_process_interface.h @@ -88,13 +88,13 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::vector<BSONObj>&& objs, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) override; StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp index 01db33c2337..694038eff96 100644 --- a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.cpp @@ -35,7 +35,6 @@ #include "mongo/db/catalog/drop_collection.h" #include "mongo/db/catalog/rename_collection.h" #include "mongo/db/concurrency/d_concurrency.h" -#include "mongo/db/concurrency/write_conflict_exception.h" #include "mongo/db/db_raii.h" #include "mongo/db/index_builds_coordinator.h" #include "mongo/db/logical_session_id_helpers.h" @@ -70,26 +69,27 @@ void ReplicaSetNodeProcessInterface::setReplicaSetNodeExecutor( replicaSetNodeExecutor(service) = std::move(executor); } -Status ReplicaSetNodeProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::vector<BSONObj>&& objs, - const WriteConcernOptions& wc, - boost::optional<OID> targetEpoch) { +Status ReplicaSetNodeProcessInterface::insert( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + const WriteConcernOptions& wc, + boost::optional<OID> targetEpoch) { auto&& opCtx = expCtx->opCtx; if (_canWriteLocally(opCtx, ns)) { - return NonShardServerProcessInterface::insert(expCtx, ns, std::move(objs), wc, targetEpoch); + return NonShardServerProcessInterface::insert( + expCtx, ns, std::move(insertCommand), wc, targetEpoch); } - BatchedCommandRequest insertCommand( - buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation)); + BatchedCommandRequest batchInsertCommand(std::move(insertCommand)); - return _executeCommandOnPrimary(opCtx, ns, std::move(insertCommand.toBSON())).getStatus(); + return _executeCommandOnPrimary(opCtx, ns, batchInsertCommand.toBSON()).getStatus(); } StatusWith<MongoProcessInterface::UpdateResult> ReplicaSetNodeProcessInterface::update( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -97,11 +97,11 @@ StatusWith<MongoProcessInterface::UpdateResult> ReplicaSetNodeProcessInterface:: auto&& opCtx = expCtx->opCtx; if (_canWriteLocally(opCtx, ns)) { return NonShardServerProcessInterface::update( - expCtx, ns, std::move(batch), wc, upsert, multi, targetEpoch); + expCtx, ns, std::move(updateCommand), wc, upsert, multi, targetEpoch); } + BatchedCommandRequest batchUpdateCommand(std::move(updateCommand)); - BatchedCommandRequest updateCommand(buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi)); - auto result = _executeCommandOnPrimary(opCtx, ns, std::move(updateCommand.toBSON())); + auto result = _executeCommandOnPrimary(opCtx, ns, batchUpdateCommand.toBSON()); if (!result.isOK()) { return result.getStatus(); } diff --git a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h index c61f654e844..55b645c59aa 100644 --- a/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/replica_set_node_process_interface.h @@ -43,6 +43,15 @@ class ReplicaSetNodeProcessInterface final : public NonShardServerProcessInterfa public: using NonShardServerProcessInterface::NonShardServerProcessInterface; + std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( + OperationContext* opCtx, const NamespaceString& ns) const override { + if (_canWriteLocally(opCtx, ns)) { + return std::make_unique<LocalWriteSizeEstimator>(); + } else { + return std::make_unique<TargetPrimaryWriteSizeEstimator>(); + } + } + static std::shared_ptr<executor::TaskExecutor> getReplicaSetNodeExecutor( ServiceContext* service); @@ -59,12 +68,13 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::vector<BSONObj>&& objs, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) final; + StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp index 69b5a111e2b..4cb90818562 100644 --- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp +++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.cpp @@ -29,8 +29,6 @@ #define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery -#include "mongo/platform/basic.h" - #include "mongo/db/pipeline/process_interface/shardsvr_process_interface.h" #include <fmt/format.h> @@ -51,7 +49,7 @@ #include "mongo/s/cluster_commands_helpers.h" #include "mongo/s/cluster_write.h" #include "mongo/s/query/document_source_merge_cursors.h" -#include "mongo/s/router.h" +#include "mongo/s/router_role.h" #include "mongo/s/stale_shard_version_helpers.h" namespace mongo { @@ -76,14 +74,11 @@ void ShardServerProcessInterface::checkRoutingInfoEpochOrThrow( catalogCache->invalidateShardOrEntireCollectionEntryForShardedCollection( nss, targetCollectionVersion, shardId); - const auto routingInfo = - uassertStatusOK(catalogCache->getCollectionRoutingInfo(expCtx->opCtx, nss)); - - const auto foundVersion = - routingInfo.isSharded() ? routingInfo.getVersion() : ChunkVersion::UNSHARDED(); + const auto cm = uassertStatusOK(catalogCache->getCollectionRoutingInfo(expCtx->opCtx, nss)); + auto foundVersion = cm.isSharded() ? cm.getVersion() : ChunkVersion::UNSHARDED(); - uassert(StaleEpochInfo(nss), - str::stream() << "Could not act as router for " << nss.ns() << ", wanted " + uassert(StaleEpochInfo(nss, targetCollectionVersion, foundVersion), + str::stream() << "Could not act as router for " << nss.ns() << ", received " << targetCollectionVersion.toString() << ", but found " << foundVersion.toString(), foundVersion.isSameCollection(targetCollectionVersion)); @@ -104,20 +99,20 @@ boost::optional<Document> ShardServerProcessInterface::lookupSingleDocument( return doLookupSingleDocument(expCtx, nss, collectionUUID, documentKey, std::move(opts)); } -Status ShardServerProcessInterface::insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, - const NamespaceString& ns, - std::vector<BSONObj>&& objs, - const WriteConcernOptions& wc, - boost::optional<OID> targetEpoch) { +Status ShardServerProcessInterface::insert( + const boost::intrusive_ptr<ExpressionContext>& expCtx, + const NamespaceString& ns, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, + const WriteConcernOptions& wc, + boost::optional<OID> targetEpoch) { BatchedCommandResponse response; BatchWriteExecStats stats; - BatchedCommandRequest insertCommand( - buildInsertOp(ns, std::move(objs), expCtx->bypassDocumentValidation)); + BatchedCommandRequest batchInsertCommand(std::move(insertCommand)); - insertCommand.setWriteConcern(wc.toBSON()); + batchInsertCommand.setWriteConcern(wc.toBSON()); - cluster::write(expCtx->opCtx, insertCommand, &stats, &response, targetEpoch); + cluster::write(expCtx->opCtx, batchInsertCommand, &stats, &response, targetEpoch); return response.toStatus(); } @@ -125,7 +120,7 @@ Status ShardServerProcessInterface::insert(const boost::intrusive_ptr<Expression StatusWith<MongoProcessInterface::UpdateResult> ShardServerProcessInterface::update( const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -133,11 +128,10 @@ StatusWith<MongoProcessInterface::UpdateResult> ShardServerProcessInterface::upd BatchedCommandResponse response; BatchWriteExecStats stats; - BatchedCommandRequest updateCommand(buildUpdateOp(expCtx, ns, std::move(batch), upsert, multi)); - - updateCommand.setWriteConcern(wc.toBSON()); + BatchedCommandRequest batchUpdateCommand(std::move(updateCommand)); + batchUpdateCommand.setWriteConcern(wc.toBSON()); - cluster::write(expCtx->opCtx, updateCommand, &stats, &response, targetEpoch); + cluster::write(expCtx->opCtx, batchUpdateCommand, &stats, &response, targetEpoch); if (auto status = response.toStatus(); status != Status::OK()) { return status; diff --git a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h index f6026f6ef3a..a08aa23777f 100644 --- a/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/shardsvr_process_interface.h @@ -53,6 +53,11 @@ public: const NamespaceString& nss, ChunkVersion targetCollectionVersion) const final; + std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( + OperationContext* opCtx, const NamespaceString& ns) const final { + return std::make_unique<TargetPrimaryWriteSizeEstimator>(); + } + std::vector<FieldPath> collectDocumentKeyFieldsActingAsRouter( OperationContext*, const NamespaceString&) const final { // We don't expect anyone to use this method on the shard itself (yet). This is currently @@ -71,23 +76,15 @@ public: const Document& documentKey, boost::optional<BSONObj> readConcern) final; - /** - * Inserts the documents 'objs' into the namespace 'ns' using the ClusterWriter for locking, - * routing, stale config handling, etc. - */ Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::vector<BSONObj>&& objs, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, const WriteConcernOptions& wc, boost::optional<OID> targetEpoch) final; - /** - * Replaces the documents matching 'queries' with 'updates' using the ClusterWriter for locking, - * routing, stale config handling, etc. - */ StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, diff --git a/src/mongo/db/pipeline/process_interface/standalone_process_interface.h b/src/mongo/db/pipeline/process_interface/standalone_process_interface.h index aceff8e6928..dc562b9089e 100644 --- a/src/mongo/db/pipeline/process_interface/standalone_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/standalone_process_interface.h @@ -41,6 +41,11 @@ public: StandaloneProcessInterface(std::shared_ptr<executor::TaskExecutor> exec) : NonShardServerProcessInterface(std::move(exec)) {} + std::unique_ptr<MongoProcessInterface::WriteSizeEstimator> getWriteSizeEstimator( + OperationContext* opCtx, const NamespaceString& ns) const final { + return std::make_unique<LocalWriteSizeEstimator>(); + } + virtual ~StandaloneProcessInterface() = default; }; diff --git a/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h b/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h index 3fe1430ac72..5c9581b1720 100644 --- a/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h +++ b/src/mongo/db/pipeline/process_interface/stub_mongo_process_interface.h @@ -52,6 +52,33 @@ public: MONGO_UNREACHABLE; } + class StubWriteSizeEstimator final : public WriteSizeEstimator { + public: + int estimateInsertHeaderSize( + const write_ops::InsertCommandRequest& insertReq) const override { + return 0; + } + + int estimateUpdateHeaderSize( + const write_ops::UpdateCommandRequest& insertReq) const override { + return 0; + } + + int estimateInsertSizeBytes(const BSONObj& insert) const override { + MONGO_UNREACHABLE; + } + + int estimateUpdateSizeBytes(const BatchObject& batchObject, + UpsertType type) const override { + MONGO_UNREACHABLE; + } + }; + + std::unique_ptr<WriteSizeEstimator> getWriteSizeEstimator( + OperationContext* opCtx, const NamespaceString& ns) const override { + return std::make_unique<StubWriteSizeEstimator>(); + } + bool isSharded(OperationContext* opCtx, const NamespaceString& ns) override { return false; } @@ -60,7 +87,7 @@ public: Status insert(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - std::vector<BSONObj>&& objs, + std::unique_ptr<write_ops::InsertCommandRequest> insertCommand, const WriteConcernOptions& wc, boost::optional<OID>) override { MONGO_UNREACHABLE; @@ -68,7 +95,7 @@ public: StatusWith<UpdateResult> update(const boost::intrusive_ptr<ExpressionContext>& expCtx, const NamespaceString& ns, - BatchedObjects&& batch, + std::unique_ptr<write_ops::UpdateCommandRequest> updateCommand, const WriteConcernOptions& wc, UpsertType upsert, bool multi, @@ -227,11 +254,11 @@ public: return BackupCursorState{UUID::gen(), boost::none, nullptr, {}}; } - void closeBackupCursor(OperationContext* opCtx, const UUID& backupId) final {} + void closeBackupCursor(OperationContext* opCtx, const UUID& backupId) override {} BackupCursorExtendState extendBackupCursor(OperationContext* opCtx, const UUID& backupId, - const Timestamp& extendTo) final { + const Timestamp& extendTo) override { return {{}}; } diff --git a/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp b/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp index a008cc92115..cc1f696fcaa 100644 --- a/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp +++ b/src/mongo/db/pipeline/resharding_initial_split_policy_test.cpp @@ -35,6 +35,8 @@ #include "mongo/db/pipeline/document_source_mock.h" #include "mongo/db/pipeline/sharded_agg_helpers.h" #include "mongo/db/s/config/initial_split_policy.h" +#include "mongo/idl/server_parameter_test_util.h" +#include "mongo/logv2/log.h" #include "mongo/s/query/sharded_agg_test_fixture.h" #include "mongo/unittest/unittest.h" @@ -47,7 +49,6 @@ const ShardId primaryShardId = ShardId("0"); TEST_F(ReshardingSplitPolicyTest, ShardKeyWithNonDottedFieldAndIdIsNotProjectedSucceeds) { auto shardKeyPattern = ShardKeyPattern(BSON("a" << 1)); - auto pipeline = Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), @@ -55,7 +56,6 @@ TEST_F(ReshardingSplitPolicyTest, ShardKeyWithNonDottedFieldAndIdIsNotProjectedS auto mockSource = DocumentSourceMock::createForTest({"{_id: 10, a: 15}", "{_id: 3, a: 5}"}, expCtx()); pipeline->addInitialSource(mockSource.get()); - // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); @@ -92,7 +92,6 @@ TEST_F(ReshardingSplitPolicyTest, ShardKeyWithIdFieldIsProjectedSucceeds) { TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithNonDottedHashedFieldSucceeds) { auto shardKeyPattern = ShardKeyPattern(BSON("a" << 1 << "b" << "hashed")); - auto pipeline = Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), @@ -100,7 +99,6 @@ TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithNonDottedHashedFieldSuccee auto mockSource = DocumentSourceMock::createForTest( {"{x: 1, b: 16, a: 15}", "{x: 2, b: 123, a: 5}"}, expCtx()); pipeline->addInitialSource(mockSource.get()); - // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); @@ -128,9 +126,9 @@ TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedFieldSucceeds) { // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.get().toBson(), BSON("a" << BSON("b" << 10) << "c" << 5)); + ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("a.b" << 10 << "c" << 5)); next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.get().toBson(), BSON("a" << BSON("b" << 20) << "c" << 1)); + ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("a.b" << 20 << "c" << 1)); ASSERT(!pipeline->getNext()); } @@ -149,11 +147,11 @@ TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedHashedFieldSucceeds) // We sample all of the documents since numSplitPoints(1) * samplingRatio (2) = 2 and the // document source has 2 chunks. So we can assert on the returned values. auto next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.get().toBson(), - BSON("a" << BSON("b" << 10 << "c" << -6548868637522515075LL) << "c" << 5)); + ASSERT_BSONOBJ_EQ(next.value().toBson(), + BSON("a.b" << 10 << "c" << 5 << "a.c" << -6548868637522515075LL)); next = pipeline->getNext(); - ASSERT_BSONOBJ_EQ(next.get().toBson(), - BSON("a" << BSON("b" << 20 << "c" << 2598032665634823220LL) << "c" << 1)); + ASSERT_BSONOBJ_EQ(next.value().toBson(), + BSON("a.b" << 20 << "c" << 1 << "a.c" << 2598032665634823220LL)); ASSERT(!pipeline->getNext()); } @@ -200,5 +198,78 @@ TEST_F(ReshardingSplitPolicyTest, SamplingSuceeds) { } } +TEST_F(ReshardingSplitPolicyTest, ShardKeyWithDottedPathAndIdIsNotProjectedSucceeds) { + auto shardKeyPattern = ShardKeyPattern(BSON("b" << 1)); + auto pipeline = + Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( + shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), + expCtx()); + auto mockSource = DocumentSourceMock::createForTest( + {"{_id: {a: 15}, b: 10}", "{_id: {a: 5}, b:1}"}, expCtx()); + pipeline->addInitialSource(mockSource.get()); + auto next = pipeline->getNext(); + ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("b" << 1)); + next = pipeline->getNext(); + ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("b" << 10)); + ASSERT(!pipeline->getNext()); +} + +TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedPathAndIdIsProjectedSucceeds) { + auto shardKeyPattern = ShardKeyPattern(BSON("_id.a" << 1 << "c" << 1)); + auto pipeline = + Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( + shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), + expCtx()); + auto mockSource = DocumentSourceMock::createForTest( + {"{_id: {a: 15}, c: 10}", "{_id: {a: 5}, c: 1}"}, expCtx()); + pipeline->addInitialSource(mockSource.get()); + auto next = pipeline->getNext(); + ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("_id.a" << 5 << "c" << 1)); + next = pipeline->getNext(); + ASSERT_BSONOBJ_EQ(next.value().toBson(), BSON("_id.a" << 15 << "c" << 10)); + ASSERT(!pipeline->getNext()); +} + +TEST_F(ReshardingSplitPolicyTest, CompoundShardKeyWithDottedHashedPathSucceeds) { + auto shardKeyPattern = ShardKeyPattern(BSON("_id.a" << 1 << "b" << 1 << "_id.b" + << "hashed")); + auto pipeline = + Pipeline::parse(ReshardingSplitPolicy::createRawPipeline( + shardKeyPattern, 2 /* samplingRatio */, 1 /* numSplitPoints */), + expCtx()); + auto mockSource = DocumentSourceMock::createForTest( + {"{x: 10, _id: {a: 20, b: 16}, b: 1}", "{x: 3, _id: {a: 10, b: 123}, b: 5}"}, expCtx()); + pipeline->addInitialSource(mockSource.get()); + + auto next = pipeline->getNext(); + ASSERT_BSONOBJ_EQ(next.value().toBson(), + BSON("_id.a" << 10 << "b" << 5 << "_id.b" << -6548868637522515075LL)); + next = pipeline->getNext(); + ASSERT_BSONOBJ_EQ(next.value().toBson(), + BSON("_id.a" << 20 << "b" << 1 << "_id.b" << 2598032665634823220LL)); + ASSERT(!pipeline->getNext()); +} + +TEST_F(ReshardingSplitPolicyTest, ReshardingSucceedsWithLimitedMemoryForSortOperation) { + RAIIServerParameterControllerForTest sortMaxMemory{ + "internalQueryMaxBlockingSortMemoryUsageBytes", 100}; + auto shardKeyPattern = ShardKeyPattern(BSON("a" << 1)); + const NamespaceString ns("reshard", "foo"); + auto pipelineDocSource = + ReshardingSplitPolicy::makePipelineDocumentSource_forTest(operationContext(), + kTestAggregateNss, + shardKeyPattern, + 3 /*numInitialChunks*/, + 2 /*samplesPerChunk*/); + auto mockSource = DocumentSourceMock::createForTest( + {"{_id: 20, a: 4}", "{_id: 30, a: 3}", "{_id: 40, a: 2}", "{_id: 50, a: 1}"}, expCtx()); + pipelineDocSource->getPipeline_forTest()->addInitialSource(mockSource.get()); + auto next = pipelineDocSource->getNext(); + ASSERT_BSONOBJ_EQ(BSON("a" << 2), next.value()); + next = pipelineDocSource->getNext(); + ASSERT_BSONOBJ_EQ(BSON("a" << 4), next.value()); + ASSERT(!pipelineDocSource->getNext()); +} + } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/resume_token.cpp b/src/mongo/db/pipeline/resume_token.cpp index cc6d3631fd3..8cc4fde72a0 100644 --- a/src/mongo/db/pipeline/resume_token.cpp +++ b/src/mongo/db/pipeline/resume_token.cpp @@ -38,7 +38,6 @@ #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/exec/document_value/value_comparator.h" #include "mongo/db/pipeline/change_stream_helpers_legacy.h" -#include "mongo/db/pipeline/document_source_change_stream_gen.h" #include "mongo/db/storage/key_string.h" #include "mongo/util/hex.h" @@ -88,7 +87,8 @@ bool ResumeTokenData::operator==(const ResumeTokenData& other) const { return clusterTime == other.clusterTime && version == other.version && tokenType == other.tokenType && txnOpIndex == other.txnOpIndex && fromInvalidate == other.fromInvalidate && uuid == other.uuid && - (Value::compare(this->eventIdentifier, other.eventIdentifier, nullptr) == 0); + (Value::compare(this->eventIdentifier, other.eventIdentifier, nullptr) == 0) && + fragmentNum == other.fragmentNum; } std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) { @@ -102,7 +102,11 @@ std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) { out << ", fromInvalidate: " << static_cast<bool>(tokenData.fromInvalidate); } out << ", uuid: " << tokenData.uuid; - out << ", eventIdentifier: " << tokenData.eventIdentifier << "}"; + out << ", eventIdentifier: " << tokenData.eventIdentifier; + if (tokenData.version >= 2) { + out << ", fragmentNum: " << tokenData.fragmentNum; + } + out << "}"; return out; } @@ -163,6 +167,14 @@ ResumeToken::ResumeToken(const ResumeTokenData& data) { } data.eventIdentifier.addToBsonObj(&builder, ""); + if (data.fragmentNum) { + uassert(7182504, + str::stream() << "Tokens of version " << data.version + << " cannot have a fragmentNum", + data.version >= 2); + builder.appendNumber("", static_cast<long long>(*data.fragmentNum)); + } + auto keyObj = builder.obj(); KeyString::Builder encodedToken(KeyString::Version::V1, keyObj, Ordering::make(BSONObj())); _hexKeyString = hexblob::encode(encodedToken.getBuffer(), encodedToken.getSize()); @@ -285,6 +297,14 @@ ResumeTokenData ResumeToken::getData() const { "Resume Token eventIdentifier is not an object", result.eventIdentifier.getType() == BSONType::Object); + if (i.more() && result.version >= 2) { + auto fragmentNum = i.next(); + uassert(7182501, + "Resume token 'fragmentNum' must be a non-negative integer.", + fragmentNum.type() == BSONType::NumberInt && fragmentNum.numberInt() >= 0); + result.fragmentNum = fragmentNum.numberInt(); + } + uassert(40646, "invalid oversized resume token", !i.more()); return result; } diff --git a/src/mongo/db/pipeline/resume_token.h b/src/mongo/db/pipeline/resume_token.h index a157c97fed9..c4e8b5d06fd 100644 --- a/src/mongo/db/pipeline/resume_token.h +++ b/src/mongo/db/pipeline/resume_token.h @@ -107,6 +107,9 @@ struct ResumeTokenData { // The eventIdentifier can be either be a document key for CRUD operations, or a more // descriptive operation details for non-CRUD operations. Value eventIdentifier; + + // Index of the current fragment, for oversized events that have been split. + boost::optional<size_t> fragmentNum; }; std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData); diff --git a/src/mongo/db/pipeline/resume_token_test.cpp b/src/mongo/db/pipeline/resume_token_test.cpp index eed85344ddd..8005911d5a9 100644 --- a/src/mongo/db/pipeline/resume_token_test.cpp +++ b/src/mongo/db/pipeline/resume_token_test.cpp @@ -491,5 +491,51 @@ TEST(ResumeToken, StringEncodingSortsCorrectly) { {ts10_4, 2, 0, lower_uuid, higherEventIdentifer}); } +TEST(ResumeToken, FragmentNumRoundTripsThroughEncodingAndDecoding) { + ResumeTokenData resumeTokenDataIn{ + Timestamp(1000, 1), 2, 0, UUID::gen(), Value(Document{{"_id", 1}})}; + + auto resumeTokenDataFragmentNone = + ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()).getData(); + + ASSERT_EQ(resumeTokenDataIn, resumeTokenDataFragmentNone); + ASSERT_EQ(ResumeToken(resumeTokenDataIn).toBSON().objsize(), + ResumeToken(resumeTokenDataFragmentNone).toBSON().objsize()); + + resumeTokenDataIn.fragmentNum = 0ULL; + auto resumeTokenDataFragment0 = + ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()).getData(); + + ASSERT_EQ(resumeTokenDataIn, resumeTokenDataFragment0); + ASSERT_EQ(ResumeToken(resumeTokenDataIn).toBSON().objsize(), + ResumeToken(resumeTokenDataFragment0).toBSON().objsize()); + + resumeTokenDataIn.fragmentNum = 1ULL; + auto resumeTokenDataFragment1 = + ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()).getData(); + + ASSERT_EQ(resumeTokenDataIn, resumeTokenDataFragment1); + ASSERT_EQ(ResumeToken(resumeTokenDataIn).toBSON().objsize(), + ResumeToken(resumeTokenDataFragment1).toBSON().objsize()); +} + +TEST(ResumeToken, NegativeFragmentNumThrows) { + ResumeTokenData resumeTokenDataIn{ + Timestamp(1000, 1), 2, 0, UUID::gen(), Value(Document{{"_id", 1}})}; + + // Large 'ResumeTokenData::fragmentNum' value will be serialized as a negative integer. + resumeTokenDataIn.fragmentNum = std::numeric_limits<size_t>::max(); + auto resumeToken = ResumeToken::parse(ResumeToken(resumeTokenDataIn).toDocument()); + + ASSERT_THROWS_CODE(resumeToken.getData(), DBException, 7182501); +} + +TEST(ResumeToken, FragmentNumInV1Throws) { + ResumeTokenData resumeTokenDataV1{ + Timestamp(1000, 1), 1, 0, UUID::gen(), Value(Document{{"_id", 1}})}; + + resumeTokenDataV1.fragmentNum = 0ULL; + ASSERT_THROWS_CODE(ResumeToken(resumeTokenDataV1), DBException, 7182504); +} } // namespace } // namespace mongo diff --git a/src/mongo/db/pipeline/semantic_analysis.cpp b/src/mongo/db/pipeline/semantic_analysis.cpp index c1613a6a85f..04aa2de4541 100644 --- a/src/mongo/db/pipeline/semantic_analysis.cpp +++ b/src/mongo/db/pipeline/semantic_analysis.cpp @@ -79,7 +79,7 @@ boost::optional<std::string> findRename(const StringMap<std::string>& renamedPat * maps the path to itself. */ StringMap<std::string> computeNamesAssumingAnyPathsNotRenamedAreUnmodified( - const StringMap<std::string>& renamedPaths, const std::set<std::string>& pathsOfInterest) { + const StringMap<std::string>& renamedPaths, const OrderedPathSet& pathsOfInterest) { StringMap<std::string> renameOut; for (auto&& ofInterest : pathsOfInterest) { if (auto name = findRename(renamedPaths, ofInterest)) { @@ -163,7 +163,7 @@ template <class Iterator> boost::optional<Iterator> lookForNestUnnestPattern( Iterator start, Iterator end, - std::set<std::string> pathsOfInterest, + OrderedPathSet pathsOfInterest, const Direction& traversalDir, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { auto replaceRootTransform = isReplaceRoot((*start).get()); @@ -252,7 +252,7 @@ template <class Iterator> std::pair<Iterator, StringMap<std::string>> multiStageRenamedPaths( Iterator start, Iterator end, - std::set<std::string> pathsOfInterest, + OrderedPathSet pathsOfInterest, const Direction& traversalDir, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none) { @@ -300,7 +300,7 @@ template <class Iterator> boost::optional<StringMap<std::string>> renamedPathsFullPipeline( Iterator start, Iterator end, - std::set<std::string> pathsOfInterest, + OrderedPathSet pathsOfInterest, const Direction& traversalDir, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { auto [itr, renameMap] = multiStageRenamedPaths( @@ -313,9 +313,9 @@ boost::optional<StringMap<std::string>> renamedPathsFullPipeline( } // namespace -std::set<std::string> extractModifiedDependencies(const std::set<std::string>& dependencies, - const std::set<std::string>& preservedPaths) { - std::set<std::string> modifiedDependencies; +OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies, + const OrderedPathSet& preservedPaths) { + OrderedPathSet modifiedDependencies; // The modified dependencies is *almost* the set difference 'dependencies' - 'preservedPaths', // except that if p in 'preservedPaths' is a "path prefix" of d in 'dependencies', then 'd' @@ -342,7 +342,7 @@ std::set<std::string> extractModifiedDependencies(const std::set<std::string>& d return modifiedDependencies; } -boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>& pathsOfInterest, +boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& pathsOfInterest, const DocumentSource& stage, const Direction& traversalDir) { auto modifiedPathsRet = stage.getModifiedPaths(); @@ -351,19 +351,11 @@ boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string> case DocumentSource::GetModPathsReturn::Type::kAllPaths: return boost::none; case DocumentSource::GetModPathsReturn::Type::kFiniteSet: { - for (auto&& modified : modifiedPathsRet.paths) { - for (auto&& ofInterest : pathsOfInterest) { - // Any overlap of the path means the path of interest is not preserved. For - // example, if the path of interest is "a.b", then a modified path of "a", - // "a.b", or "a.b.c" would all signal that "a.b" is not preserved. - if (ofInterest == modified || - expression::isPathPrefixOf(ofInterest, modified) || - expression::isPathPrefixOf(modified, ofInterest)) { - // This stage modifies at least one of the fields which the caller is - // interested in, bail out. - return boost::none; - } - } + // Any overlap of the path means the path of interest is not preserved. For + // example, if the path of interest is "a.b", then a modified path of "a", + // "a.b", or "a.b.c" would all signal that "a.b" is not preserved. + if (!expression::areIndependent(modifiedPathsRet.paths, pathsOfInterest)) { + return boost::none; } // None of the paths of interest were modified, construct the result map, mapping @@ -401,7 +393,7 @@ boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string> boost::optional<StringMap<std::string>> renamedPaths( const Pipeline::SourceContainer::const_iterator start, const Pipeline::SourceContainer::const_iterator end, - const std::set<std::string>& pathsOfInterest, + const OrderedPathSet& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { return renamedPathsFullPipeline( start, end, pathsOfInterest, Direction::kForward, additionalStageValidatorCallback); @@ -410,7 +402,7 @@ boost::optional<StringMap<std::string>> renamedPaths( boost::optional<StringMap<std::string>> renamedPaths( const Pipeline::SourceContainer::const_reverse_iterator start, const Pipeline::SourceContainer::const_reverse_iterator end, - const std::set<std::string>& pathsOfInterest, + const OrderedPathSet& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { return renamedPathsFullPipeline( start, end, pathsOfInterest, Direction::kBackward, additionalStageValidatorCallback); @@ -420,7 +412,7 @@ std::pair<Pipeline::SourceContainer::const_iterator, StringMap<std::string>> findLongestViablePrefixPreservingPaths( const Pipeline::SourceContainer::const_iterator start, const Pipeline::SourceContainer::const_iterator end, - const std::set<std::string>& pathsOfInterest, + const OrderedPathSet& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback) { return multiStageRenamedPaths( start, end, pathsOfInterest, Direction::kForward, additionalStageValidatorCallback); diff --git a/src/mongo/db/pipeline/semantic_analysis.h b/src/mongo/db/pipeline/semantic_analysis.h index 1931800b9df..befbddac923 100644 --- a/src/mongo/db/pipeline/semantic_analysis.h +++ b/src/mongo/db/pipeline/semantic_analysis.h @@ -57,7 +57,7 @@ enum class Direction { kForward, kBackward }; * the pipeline, and direction is backward. Say nextStage preserves all the paths but renamed "a" to * "b"; we would return a mapping b-->a. */ -boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string>& pathsOfInterest, +boost::optional<StringMap<std::string>> renamedPaths(const OrderedPathSet& pathsOfInterest, const DocumentSource& stage, const Direction& traversalDir); /** @@ -72,7 +72,7 @@ boost::optional<StringMap<std::string>> renamedPaths(const std::set<std::string> boost::optional<StringMap<std::string>> renamedPaths( Pipeline::SourceContainer::const_iterator start, Pipeline::SourceContainer::const_iterator end, - const std::set<std::string>& pathsOfInterest, + const OrderedPathSet& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none); @@ -89,7 +89,7 @@ boost::optional<StringMap<std::string>> renamedPaths( boost::optional<StringMap<std::string>> renamedPaths( Pipeline::SourceContainer::const_reverse_iterator start, Pipeline::SourceContainer::const_reverse_iterator end, - const std::set<std::string>& pathsOfInterest, + const OrderedPathSet& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none); @@ -104,7 +104,7 @@ boost::optional<StringMap<std::string>> renamedPaths( std::pair<Pipeline::SourceContainer::const_iterator, StringMap<std::string>> findLongestViablePrefixPreservingPaths(Pipeline::SourceContainer::const_iterator start, Pipeline::SourceContainer::const_iterator end, - const std::set<std::string>& pathsOfInterest, + const OrderedPathSet& pathsOfInterest, boost::optional<std::function<bool(DocumentSource*)>> additionalStageValidatorCallback = boost::none); @@ -115,10 +115,9 @@ findLongestViablePrefixPreservingPaths(Pipeline::SourceContainer::const_iterator * For example, extractModifiedDependencies({'a', 'b', 'c.d', 'e'}, {'a', 'b.c', c'}) returns * {'b', 'e'}, since 'b' and 'e' are not preserved (only 'b.c' is preserved). */ -std::set<std::string> extractModifiedDependencies(const std::set<std::string>& dependencies, - const std::set<std::string>& preservedPaths); +OrderedPathSet extractModifiedDependencies(const OrderedPathSet& dependencies, + const OrderedPathSet& preservedPaths); -bool pathSetContainsOverlappingPath(const std::set<std::string>& paths, - const std::string& targetPath); +bool pathSetContainsOverlappingPath(const OrderedPathSet& paths, const std::string& targetPath); } // namespace mongo::semantic_analysis diff --git a/src/mongo/db/pipeline/semantic_analysis_test.cpp b/src/mongo/db/pipeline/semantic_analysis_test.cpp index ca95d9098a2..0b6e4d224bf 100644 --- a/src/mongo/db/pipeline/semantic_analysis_test.cpp +++ b/src/mongo/db/pipeline/semantic_analysis_test.cpp @@ -54,7 +54,7 @@ public: GetModPathsReturn getModifiedPaths() const final { // Pretend this stage simply renames the "a" field to be "b", leaving the value of "a" the // same. This would be the equivalent of an {$addFields: {b: "$a"}}. - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{}, {{"b", "a"}}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{}, {{"b", "a"}}}; } }; @@ -132,9 +132,7 @@ public: : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kAllExcept, - std::set<std::string>{"e", "f", "g"}, - {{"d", "c"}}}; + return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{"e", "f", "g"}, {{"d", "c"}}}; } }; @@ -196,7 +194,7 @@ public: : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kAllExcept, std::set<std::string>{"f.g"}, {{"e", "c.d"}}}; + return {GetModPathsReturn::Type::kAllExcept, OrderedPathSet{"f.g"}, {{"e", "c.d"}}}; } }; @@ -295,7 +293,7 @@ public: : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kFiniteSet, std::set<std::string>{"c.d"}, {{"x.y", "a"}}}; + return {GetModPathsReturn::Type::kFiniteSet, OrderedPathSet{"c.d"}, {{"x.y", "a"}}}; } }; @@ -386,7 +384,7 @@ public: ModifiesAllPaths(const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kAllPaths, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kAllPaths, OrderedPathSet{}, {}}; } }; @@ -415,7 +413,7 @@ public: ModificationsUnknown(const boost::intrusive_ptr<ExpressionContext>& expCtx) : DocumentSourceTestOptimizations(expCtx) {} GetModPathsReturn getModifiedPaths() const final { - return {GetModPathsReturn::Type::kNotSupported, std::set<std::string>{}, {}}; + return {GetModPathsReturn::Type::kNotSupported, OrderedPathSet{}, {}}; } }; diff --git a/src/mongo/db/pipeline/sharded_agg_helpers.cpp b/src/mongo/db/pipeline/sharded_agg_helpers.cpp index 9a7b4df2242..42e8ae24d32 100644 --- a/src/mongo/db/pipeline/sharded_agg_helpers.cpp +++ b/src/mongo/db/pipeline/sharded_agg_helpers.cpp @@ -61,7 +61,7 @@ #include "mongo/s/query/cluster_query_knobs_gen.h" #include "mongo/s/query/document_source_merge_cursors.h" #include "mongo/s/query/establish_cursors.h" -#include "mongo/s/router.h" +#include "mongo/s/router_role.h" #include "mongo/s/stale_exception.h" #include "mongo/s/transaction_router.h" #include "mongo/util/fail_point.h" @@ -414,7 +414,7 @@ void moveEligibleStreamingStagesBeforeSortOnShards(Pipeline* shardPipe, // Expected last stage on the shards to be a $sort. return; } - auto sortPaths = sortPattern.getFieldNames<std::set<std::string>>(); + auto sortPaths = sortPattern.getFieldNames<OrderedPathSet>(); auto firstMergeStage = mergePipe->getSources().cbegin(); std::function<bool(DocumentSource*)> distributedPlanLogicCallback = [](DocumentSource* stage) { return !static_cast<bool>(stage->distributedPlanLogic()); @@ -561,7 +561,7 @@ void limitFieldsSentFromShardsToMerger(Pipeline* shardPipe, Pipeline* mergePipe) } bool stageCanRunInParallel(const boost::intrusive_ptr<DocumentSource>& stage, - const std::set<std::string>& nameOfShardKeyFieldsUponEntryToStage) { + const OrderedPathSet& nameOfShardKeyFieldsUponEntryToStage) { if (stage->distributedPlanLogic()) { return stage->canRunInParallelBeforeWriteStage(nameOfShardKeyFieldsUponEntryToStage); } else { @@ -598,7 +598,7 @@ BSONObj buildNewKeyPattern(const ShardKeyPattern& shardKey, StringMap<std::strin } StringMap<std::string> computeShardKeyRenameMap(const Pipeline* mergePipeline, - std::set<std::string>&& pathsOfShardKey) { + OrderedPathSet&& pathsOfShardKey) { auto traversalStart = mergePipeline->getSources().crbegin(); auto traversalEnd = mergePipeline->getSources().crend(); const auto leadingGroup = @@ -626,7 +626,7 @@ StringMap<std::string> computeShardKeyRenameMap(const Pipeline* mergePipeline, * * Purposefully takes 'shardKeyPaths' by value so that it can be modified throughout. */ -bool anyStageModifiesShardKeyOrNeedsMerge(std::set<std::string> shardKeyPaths, +bool anyStageModifiesShardKeyOrNeedsMerge(OrderedPathSet shardKeyPaths, const Pipeline* mergePipeline) { const auto& stages = mergePipeline->getSources(); for (auto it = stages.crbegin(); it != stages.crend(); ++it) { @@ -653,7 +653,7 @@ boost::optional<ShardedExchangePolicy> walkPipelineBackwardsTrackingShardKey( OperationContext* opCtx, const Pipeline* mergePipeline, const ChunkManager& chunkManager) { const ShardKeyPattern& shardKey = chunkManager.getShardKeyPattern(); - std::set<std::string> shardKeyPaths; + OrderedPathSet shardKeyPaths; for (auto&& path : shardKey.getKeyPatternFields()) { shardKeyPaths.emplace(path->dottedField().toString()); } @@ -769,10 +769,16 @@ std::unique_ptr<Pipeline, PipelineDeleter> targetShardsAndAddMergeCursors( LiteParsedPipeline liteParsedPipeline(aggRequest); auto hasChangeStream = liteParsedPipeline.hasChangeStream(); + auto startsWithDocuments = liteParsedPipeline.startsWithDocuments(); auto shardDispatchResults = dispatchShardPipeline(aggregation_request_helper::serializeToCommandDoc(aggRequest), hasChangeStream, + startsWithDocuments, std::move(pipeline), + // Even if the overall operation is an explain, callers of this + // function always intend to actually execute a regular agg command + // and merge the results with $mergeCursors. + boost::none /*explain*/, shardTargetingPolicy, std::move(readConcern)); @@ -955,6 +961,7 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont const SplitPipeline& splitPipeline, const boost::optional<ShardedExchangePolicy> exchangeSpec, bool needsMerge, + boost::optional<ExplainOptions::Verbosity> explain, boost::optional<BSONObj> readConcern) { // Create the command for the shards. MutableDocument targetedCmd(serializedCommand); @@ -986,28 +993,22 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont targetedCmd[AggregateCommandRequest::kExchangeFieldName] = exchangeSpec ? Value(exchangeSpec->exchangeSpec.toBSON()) : Value(); - auto shardCommand = genericTransformForShards(std::move(targetedCmd), - expCtx, - expCtx->explain, - expCtx->getCollatorBSON(), - std::move(readConcern)); + auto shardCommand = genericTransformForShards( + std::move(targetedCmd), expCtx, explain, expCtx->getCollatorBSON(), std::move(readConcern)); // Apply RW concern to the final shard command. return applyReadWriteConcern(expCtx->opCtx, - true, /* appendRC */ - !expCtx->explain, /* appendWC */ + true, /* appendRC */ + !explain, /* appendWC */ shardCommand); } -/** - * Targets shards for the pipeline and returns a struct with the remote cursors or results, and - * the pipeline that will need to be executed to merge the results from the remotes. If a stale - * shard version is encountered, refreshes the routing table and tries again. - */ DispatchShardPipelineResults dispatchShardPipeline( Document serializedCommand, bool hasChangeStream, + bool startsWithDocuments, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, + boost::optional<ExplainOptions::Verbosity> explain, ShardTargetingPolicy shardTargetingPolicy, boost::optional<BSONObj> readConcern) { auto expCtx = pipeline->getContext(); @@ -1051,7 +1052,7 @@ DispatchShardPipelineResults dispatchShardPipeline( : expCtx->getCollatorBSON(); // Determine whether we can run the entire aggregation on a single shard. - const bool mustRunOnAll = mustRunOnAllShards(expCtx->ns, hasChangeStream); + const bool mustRunOnAll = mustRunOnAllShards(expCtx->ns, hasChangeStream, startsWithDocuments); std::set<ShardId> shardIds = getTargetedShards( expCtx, mustRunOnAll, executionNsRoutingInfo, shardQuery, shardTargetingCollation); @@ -1103,10 +1104,11 @@ DispatchShardPipelineResults dispatchShardPipeline( *splitPipelines, exchangeSpec, true /* needsMerge */, + explain, std::move(readConcern)) : createPassthroughCommandForShard(expCtx, serializedCommand, - expCtx->explain, + explain, pipeline.get(), expCtx->getCollatorBSON(), std::move(readConcern), @@ -1139,7 +1141,7 @@ DispatchShardPipelineResults dispatchShardPipeline( shardIds.size() > 0); // Explain does not produce a cursor, so instead we scatter-gather commands to the shards. - if (expCtx->explain) { + if (explain) { if (mustRunOnAll) { // Some stages (such as $currentOp) need to be broadcast to all shards, and // should not participate in the shard version protocol. @@ -1154,7 +1156,7 @@ DispatchShardPipelineResults dispatchShardPipeline( // shards, and should participate in the shard version protocol. invariant(executionNsRoutingInfo); shardResults = - scatterGatherVersionedTargetByRoutingTable(opCtx, + scatterGatherVersionedTargetByRoutingTable(expCtx, expCtx->ns.db(), expCtx->ns, *executionNsRoutingInfo, @@ -1175,7 +1177,7 @@ DispatchShardPipelineResults dispatchShardPipeline( targetedCommand, ReadPreferenceSetting::get(opCtx)); - } catch (const StaleConfigException& e) { + } catch (const ExceptionFor<ErrorCodes::StaleConfig>& e) { // Check to see if the command failed because of a stale shard version or something // else. auto staleInfo = e.extraInfo<StaleConfigInfo>(); @@ -1486,10 +1488,13 @@ BSONObj targetShardsForExplain(Pipeline* ownedPipeline) { AggregateCommandRequest aggRequest(expCtx->ns, rawStages); LiteParsedPipeline liteParsedPipeline(aggRequest); auto hasChangeStream = liteParsedPipeline.hasChangeStream(); + auto startsWithDocuments = liteParsedPipeline.startsWithDocuments(); auto shardDispatchResults = dispatchShardPipeline(aggregation_request_helper::serializeToCommandDoc(aggRequest), hasChangeStream, - std::move(pipeline)); + startsWithDocuments, + std::move(pipeline), + expCtx->explain); BSONObjBuilder explainBuilder; auto appendStatus = appendExplainResults(std::move(shardDispatchResults), expCtx, &explainBuilder); @@ -1523,11 +1528,13 @@ Shard::RetryPolicy getDesiredRetryPolicy(OperationContext* opCtx) { return Shard::RetryPolicy::kIdempotent; } -bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream) { +bool mustRunOnAllShards(const NamespaceString& nss, + bool hasChangeStream, + bool startsWithDocuments) { // The following aggregations must be routed to all shards: // - Any collectionless aggregation, such as non-localOps $currentOp. // - Any aggregation which begins with a $changeStream stage. - return nss.isCollectionlessAggregateNS() || hasChangeStream; + return !startsWithDocuments && (nss.isCollectionlessAggregateNS() || hasChangeStream); } std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( @@ -1583,11 +1590,14 @@ std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( [&](OperationContext* opCtx, const ChunkManager& cm) { auto pipelineToTarget = pipeline->clone(); - if (!cm.isSharded()) { + if (!cm.isSharded() && expCtx->ns != NamespaceString::kConfigsvrCollectionsNamespace) { // If the collection is unsharded and we are on the primary, we should be able to // do a local read. The primary may be moved right after the primary shard check, // but the local read path will do a db version check before it establishes a cursor // to catch this case and ensure we fail to read locally. + // There is the case where we are in config.collections (collection unsharded) and + // we want to broadcast to all shards. In this case we don't want to do a local read + // and we must target the config servers. try { auto expectUnshardedCollection( expCtx->mongoProcessInterface->expectUnshardedCollectionInScope( diff --git a/src/mongo/db/pipeline/sharded_agg_helpers.h b/src/mongo/db/pipeline/sharded_agg_helpers.h index 5f30f6f6a93..12945397b59 100644 --- a/src/mongo/db/pipeline/sharded_agg_helpers.h +++ b/src/mongo/db/pipeline/sharded_agg_helpers.h @@ -120,11 +120,26 @@ SplitPipeline splitPipeline(std::unique_ptr<Pipeline, PipelineDeleter> pipeline) * Targets shards for the pipeline and returns a struct with the remote cursors or results, and * the pipeline that will need to be executed to merge the results from the remotes. If a stale * shard version is encountered, refreshes the routing table and tries again. + * + * Although the 'pipeline' has an 'ExpressionContext' which indicates whether this operation is an + * explain (and if it is an explain what the verbosity is), the caller must explicitly indicate + * whether it wishes to dispatch a regular aggregate command or an explain command using the + * explicit 'explain' parameter. The reason for this is that in some contexts, the caller wishes to + * dispatch a regular agg command rather than an explain command even if the top-level operation is + * an explain. Consider the example of an explain that contains a stage like this: + * + * {$unionWith: {coll: "innerShardedColl", pipeline: <sub-pipeline>}} + * + * The explain works by first executing the inner and outer subpipelines in order to gather runtime + * statistics. While dispatching the inner pipeline, we must dispatch it not as an explain but as a + * regular agg command so that the runtime stats are accurate. */ DispatchShardPipelineResults dispatchShardPipeline( Document serializedCommand, bool hasChangeStream, + bool startsWithDocuments, std::unique_ptr<Pipeline, PipelineDeleter> pipeline, + boost::optional<ExplainOptions::Verbosity> explain, ShardTargetingPolicy shardTargetingPolicy = ShardTargetingPolicy::kAllowed, boost::optional<BSONObj> readConcern = boost::none); @@ -142,6 +157,7 @@ BSONObj createCommandForTargetedShards(const boost::intrusive_ptr<ExpressionCont const SplitPipeline& splitPipeline, boost::optional<ShardedExchangePolicy> exchangeSpec, bool needsMerge, + boost::optional<ExplainOptions::Verbosity> explain, boost::optional<BSONObj> readConcern = boost::none); /** @@ -179,7 +195,7 @@ StatusWith<ChunkManager> getExecutionNsRoutingInfo(OperationContext* opCtx, /** * Returns true if an aggregation over 'nss' must run on all shards. */ -bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream); +bool mustRunOnAllShards(const NamespaceString& nss, bool hasChangeStream, bool startsWithDocuments); /** * Retrieves the desired retry policy based on whether the default writeConcern is set on 'opCtx'. @@ -202,6 +218,11 @@ std::unique_ptr<Pipeline, PipelineDeleter> attachCursorToPipeline( * beginning with that DocumentSourceMergeCursors stage. Note that one of the 'remote' cursors might * be this node itself. * + * Even if the ExpressionContext indicates that this operation is explain, this function still + * dispatches the pipeline as a non-explain, since it must open cursors on the remote nodes and + * merge them with a $mergeCursors. If the caller's intent is to dispatch an explain command, it + * must use a different helper. + * * Use the AggregateCommandRequest alternative for 'targetRequest' to explicitly specify command * options (e.g. read concern) to the shards when establishing remote cursors. Note that doing so * incurs the cost of parsing the pipeline. diff --git a/src/mongo/db/pipeline/skip_and_limit.cpp b/src/mongo/db/pipeline/skip_and_limit.cpp index e9e7e9772ce..0f9f7d3aca7 100644 --- a/src/mongo/db/pipeline/skip_and_limit.cpp +++ b/src/mongo/db/pipeline/skip_and_limit.cpp @@ -82,8 +82,21 @@ Pipeline::SourceContainer::iterator eraseAndStich(Pipeline::SourceContainer::ite } // namespace -boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::iterator itr, - Pipeline::SourceContainer* container) { +/** + * If there are any $limit stages that could be logically swapped forward to the position of the + * pipeline pointed to by 'itr' without changing the meaning of the query, removes these $limit + * stages from the Pipeline and returns the resulting limit. A single limit value is computed by + * taking the minimum after swapping each individual $limit stage forward. + * + * This method also implements the ability to swap a $limit before a $skip, by adding the value of + * the $skip to the value of the $limit. + * + * If shouldModifyPipeline is false, this method does not swap any stages but rather just returns + * the single limit value described above. + */ +boost::optional<long long> extractLimitForPushdownHelper(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container, + bool shouldModifyPipeline) { int64_t skipSum = 0; boost::optional<long long> minLimit; while (itr != container->end()) { @@ -104,7 +117,11 @@ boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::it minLimit = std::min(static_cast<long long>(safeSum), *minLimit); } - itr = eraseAndStich(itr, container); + if (shouldModifyPipeline) { + itr = eraseAndStich(itr, container); + } else { + ++itr; + } } else if (!nextStage->constraints().canSwapWithSkippingOrLimitingStage) { break; } else { @@ -115,6 +132,16 @@ boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::it return minLimit; } +boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container) { + return extractLimitForPushdownHelper(itr, container, true /* shouldModifyPipeline */); +} + +boost::optional<long long> getUserLimit(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container) { + return extractLimitForPushdownHelper(itr, container, false /* shouldModifyPipeline */); +} + boost::optional<long long> extractSkipForPushdown(Pipeline::SourceContainer::iterator itr, Pipeline::SourceContainer* container) { boost::optional<long long> skipSum; diff --git a/src/mongo/db/pipeline/skip_and_limit.h b/src/mongo/db/pipeline/skip_and_limit.h index 2a3ecec5982..07023afa1d9 100644 --- a/src/mongo/db/pipeline/skip_and_limit.h +++ b/src/mongo/db/pipeline/skip_and_limit.h @@ -89,6 +89,13 @@ boost::optional<long long> extractLimitForPushdown(Pipeline::SourceContainer::it Pipeline::SourceContainer* container); /** + * This is similar to extractLimitForPushdown, except that it should be used when the caller does + * not want to modify the pipeline but still obtain the calculated limit value of the query. + */ +boost::optional<long long> getUserLimit(Pipeline::SourceContainer::iterator itr, + Pipeline::SourceContainer* container); + +/** * If there are any $skip stages that could be logically swapped forward to the position of the * pipeline pointed to by 'itr' without changing the meaning of the query, removes these $skip * stages from the Pipeline and returns the resulting skip. A single skip value is computed by diff --git a/src/mongo/db/pipeline/stage_constraints.h b/src/mongo/db/pipeline/stage_constraints.h index 17537069d51..475279869c0 100644 --- a/src/mongo/db/pipeline/stage_constraints.h +++ b/src/mongo/db/pipeline/stage_constraints.h @@ -55,11 +55,10 @@ struct StageConstraints { enum class PositionRequirement { kNone, kFirst, - // User can specify this stage anywhere, as long as the system can move the stage to be - // first. If pipeline optimization is disabled, then the stage must be first prior to - // optimization. - kFirstAfterOptimization, - kLast + kLast, + // Stages with 'kCustom' requirement must also implement the 'validatePipelinePosition()' + // method which is called during pipeline validation. + kCustom }; /** @@ -93,10 +92,15 @@ struct StageConstraints { /** * A ChangeStreamRequirement determines whether a particular stage is itself a ChangeStream - * stage, whether it is allowed to exist in a $changeStream pipeline, or whether it is - * denylisted from $changeStream. + * stage, whether it is allowed to exist in a $changeStream pipeline, or whether it can only + * exist in a change stream pipeline. */ - enum class ChangeStreamRequirement { kChangeStreamStage, kAllowlist, kDenylist }; + enum class ChangeStreamRequirement { + kChangeStreamStage, // This stage is an actual change stream stage. + kAllowlist, // This stage is permitted in a change stream pipeline. + kDenylist, // This stage is banned from change stream pipelines. + kRequiresChangeStream // This stage is only allowed in a change stream pipeline. + }; /** * A FacetRequirement indicates whether this stage may be used within a $facet pipeline. @@ -249,6 +253,13 @@ struct StageConstraints { } /** + * True if this stage must run in a pipeline which starts with $changeStream. + */ + bool requiresChangeStream() const { + return changeStreamRequirement == ChangeStreamRequirement::kRequiresChangeStream; + } + + /** * Returns true if this stage is legal when the readConcern level is "snapshot" or when this * aggregation is being run within a multi-document transaction. */ @@ -343,6 +354,11 @@ struct StageConstraints { // Indicates that a stage is allowed within a pipeline-stlye update. bool isAllowedWithinUpdatePipeline = false; + // If true, then this stage may only appear in the pipeline once, though it can appear at an + // arbitrary position. It is not necessary to consider this for stages which have a strict + // PositionRequirement, since the presence of a second stage will violate that constraint. + bool canAppearOnlyOnceInPipeline = false; + // Indicates that a stage does not modify anything to do with a sort and can be done before a // following merge sort. bool preservesOrderAndMetadata = false; @@ -358,6 +374,8 @@ struct StageConstraints { isIndependentOfAnyCollection == other.isIndependentOfAnyCollection && canSwapWithMatch == other.canSwapWithMatch && canSwapWithSkippingOrLimitingStage == other.canSwapWithSkippingOrLimitingStage && + canSwapWithSingleDocTransform == other.canSwapWithSingleDocTransform && + canAppearOnlyOnceInPipeline == other.canAppearOnlyOnceInPipeline && isAllowedWithinUpdatePipeline == other.isAllowedWithinUpdatePipeline && unionRequirement == other.unionRequirement && preservesOrderAndMetadata == other.preservesOrderAndMetadata; diff --git a/src/mongo/db/pipeline/window_function/partition_iterator.cpp b/src/mongo/db/pipeline/window_function/partition_iterator.cpp index d8992aec28c..3b27b4ab03e 100644 --- a/src/mongo/db/pipeline/window_function/partition_iterator.cpp +++ b/src/mongo/db/pipeline/window_function/partition_iterator.cpp @@ -116,8 +116,8 @@ optional<Document> PartitionIterator::operator[](int index) { for (int i = _cache->getHighestIndex(); i < docDesired; i++) { // Pull in document from prior stage. getNextDocument(); - // Check for EOF or the next partition. - if (_state == IteratorState::kAwaitingAdvanceToNext || + // Check whether the next document is available. + if (isPaused() || _state == IteratorState::kAwaitingAdvanceToNext || _state == IteratorState::kAwaitingAdvanceToEOF) { return boost::none; } @@ -163,6 +163,7 @@ PartitionIterator::AdvanceResult PartitionIterator::advanceInternal() { // whether to pull from the prior stage. switch (_state) { case IteratorState::kNotInitialized: + case IteratorState::kPauseExecution: case IteratorState::kIntraPartition: // Pull in the next document and advance the pointer. getNextDocument(); @@ -301,8 +302,13 @@ optional<std::pair<int, int>> PartitionIterator::getEndpointsRangeBased( for (int i = start; (doc = (*this)[i]); ++i) { Value v = (*_sortExpr)->evaluate(*doc, &_expCtx->variables); if (!lessThan(v, threshold)) { - // This is the first doc we've scanned that crossed the threshold. - return i; + // This is the first doc we've scanned that crossed the threshold, + // so it's the first doc in the window (as long as it's the expected type). + if (hasExpectedType(v)) { + return i; + } else { + return boost::none; + } } } // We scanned every document in the partition, and none crossed the @@ -467,9 +473,12 @@ void PartitionIterator::getNextDocument() { return; } - if (!getNextRes.isAdvanced()) + if (getNextRes.isPaused()) { + _state = IteratorState::kPauseExecution; return; + } + tassert(7169100, "getNextResult must have advanced", getNextRes.isAdvanced()); auto doc = getNextRes.releaseDocument(); // Greedily populate the internal document cache to enable easier memory tracking versus @@ -477,7 +486,7 @@ void PartitionIterator::getNextDocument() { doc.fillCache(); if (_partitionExpr) { - if (_state == IteratorState::kNotInitialized) { + if (!_partitionComparator) { _partitionComparator = std::make_unique<PartitionKeyComparator>(_expCtx, *_partitionExpr, doc); _nextPartitionDoc = std::move(doc); diff --git a/src/mongo/db/pipeline/window_function/partition_iterator.h b/src/mongo/db/pipeline/window_function/partition_iterator.h index 128901834ee..28e0e6a6242 100644 --- a/src/mongo/db/pipeline/window_function/partition_iterator.h +++ b/src/mongo/db/pipeline/window_function/partition_iterator.h @@ -77,6 +77,13 @@ public: return (*this)[0]; } + /** + * Returns true if iterator execution is paused. + */ + bool isPaused() { + return _state == IteratorState::kPauseExecution; + } + enum class AdvanceResult { kAdvanced, kNewPartition, @@ -287,6 +294,9 @@ private: enum class IteratorState { // Default state, no documents have been pulled into the cache. kNotInitialized, + // Input sources do not have a result to be processed yet, but there may be more results in + // the future. + kPauseExecution, // Iterating the current partition. We don't know where the current partition ends, or // whether it's the last partition. kIntraPartition, diff --git a/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp b/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp index cb6b5bcf1bf..4ba0a692e47 100644 --- a/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp +++ b/src/mongo/db/pipeline/window_function/partition_iterator_test.cpp @@ -499,7 +499,7 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForDocumentIteratorCache) { const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); // Pull in the first document, and verify the reported size of the iterator is roughly double // the size of the document. The size of the iterator is double the size of the document because @@ -525,7 +525,7 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForArraysInDocumentIteratorCach const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); // Pull in the first document, and verify the reported size of the iterator is roughly // triple the size of the document. The reason for this is that 'largeStr' is cached twice; once @@ -550,7 +550,7 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForNestedArraysInDocumentIterat const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); // Pull in the first document, and verify the reported size of the iterator is roughly // triple the size of the document. The reason for this is that 'largeStr' is cached twice; once @@ -575,7 +575,7 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForNestedObjInDocumentIteratorC const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); [[maybe_unused]] auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); // Pull in the first document, and verify the reported size. TODO SERVER-57011: The approximate // size should not double count the nested strings. @@ -592,7 +592,7 @@ TEST_F(PartitionIteratorTest, MemoryUsageAccountsForReleasedDocuments) { const auto mock = DocumentSourceMock::createForTest(docs, getExpCtx()); auto accessor = makeDefaultAccessor(mock, boost::none); - size_t initialDocSize = docs[0].getDocument().getApproximateSize(); + size_t initialDocSize = docs[0].getDocument().getCurrentApproximateSize(); // Pull in the first document, and verify the reported size of the iterator is roughly double // the size of the document. diff --git a/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp b/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp index 62ec29b4d9c..7599842de57 100644 --- a/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp +++ b/src/mongo/db/pipeline/window_function/spillable_cache_test.cpp @@ -30,7 +30,7 @@ #include "mongo/platform/basic.h" #include "mongo/db/catalog_raii.h" -#include "mongo/db/concurrency/write_conflict_exception.h" +#include "mongo/db/concurrency/exception_util.h" #include "mongo/db/exec/document_value/document_value_test_util.h" #include "mongo/db/pipeline/aggregation_mongod_context_fixture.h" #include "mongo/db/pipeline/window_function/spillable_cache.h" diff --git a/src/mongo/db/pipeline/window_function/window_function_expression.cpp b/src/mongo/db/pipeline/window_function/window_function_expression.cpp index 54f0552b59b..0d43689c977 100644 --- a/src/mongo/db/pipeline/window_function/window_function_expression.cpp +++ b/src/mongo/db/pipeline/window_function/window_function_expression.cpp @@ -37,6 +37,7 @@ #include "mongo/db/pipeline/document_source_set_window_fields_gen.h" #include "mongo/db/pipeline/lite_parsed_document_source.h" #include "mongo/db/query/query_feature_flags_gen.h" +#include "mongo/db/stats/counters.h" #include "mongo/db/pipeline/window_function/partition_iterator.h" #include "mongo/db/pipeline/window_function/window_function_exec.h" @@ -152,6 +153,7 @@ intrusive_ptr<Expression> Expression::parse(BSONObj obj, assertLanguageFeatureIsAllowed( opCtx, exprName, allowedWithApi, AllowedWithClientType::kAny); + expCtx->incrementWindowAccumulatorExprCounter(exprName); return parser(obj, sortBy, expCtx); } @@ -187,6 +189,7 @@ void Expression::registerParser( AllowedWithApiStrict allowedWithApi) { invariant(parserMap.find(functionName) == parserMap.end()); ExpressionParserRegistration r{parser, requiredMinVersion, allowedWithApi}; + operatorCountersWindowAccumulatorExpressions.addCounter(functionName); parserMap.emplace(std::move(functionName), std::move(r)); } diff --git a/src/mongo/db/pipeline/window_function/window_function_expression.h b/src/mongo/db/pipeline/window_function/window_function_expression.h index 0d638ac1350..261d4212fe5 100644 --- a/src/mongo/db/pipeline/window_function/window_function_expression.h +++ b/src/mongo/db/pipeline/window_function/window_function_expression.h @@ -555,7 +555,7 @@ protected: case TimeUnit::year: case TimeUnit::quarter: case TimeUnit::month: - uasserted(5490704, "unit must be 'week' or smaller"); + uasserted(5490710, "unit must be 'week' or smaller"); // Only these time units are allowed. case TimeUnit::week: case TimeUnit::day: @@ -803,11 +803,11 @@ public: } boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final { - MONGO_UNREACHABLE_TASSERT(5490701); + MONGO_UNREACHABLE_TASSERT(5490704); } std::unique_ptr<WindowFunctionState> buildRemovable() const final { - MONGO_UNREACHABLE_TASSERT(5490702); + MONGO_UNREACHABLE_TASSERT(5490705); } Value serialize(boost::optional<ExplainOptions::Verbosity> explain) const final { @@ -851,11 +851,11 @@ public: } boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final { - MONGO_UNREACHABLE_TASSERT(5490701); + MONGO_UNREACHABLE_TASSERT(5490706); } std::unique_ptr<WindowFunctionState> buildRemovable() const final { - MONGO_UNREACHABLE_TASSERT(5490702); + MONGO_UNREACHABLE_TASSERT(5490707); } }; @@ -873,11 +873,11 @@ public: } boost::intrusive_ptr<AccumulatorState> buildAccumulatorOnly() const final { - MONGO_UNREACHABLE_TASSERT(5490701); + MONGO_UNREACHABLE_TASSERT(5490708); } std::unique_ptr<WindowFunctionState> buildRemovable() const final { - MONGO_UNREACHABLE_TASSERT(5490702); + MONGO_UNREACHABLE_TASSERT(5490709); } }; diff --git a/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h b/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h index fbf77c59fa2..64d3319efb9 100644 --- a/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h +++ b/src/mongo/db/pipeline/window_function/window_function_top_bottom_n.h @@ -56,17 +56,17 @@ public: explicit WindowFunctionTopBottomN(ExpressionContext* const expCtx, SortPattern sp, long long n) : WindowFunctionState(expCtx), _acc(expCtx, std::move(sp), true) { _acc.startNewGroup(Value(n)); - _memUsageBytes = sizeof(*this); + updateMemUsage(); } void add(Value value) final { _acc.process(value, false); - _memUsageBytes = _acc.getMemUsage(); + updateMemUsage(); } void remove(Value value) final { _acc.remove(value); - _memUsageBytes = _acc.getMemUsage(); + updateMemUsage(); } Value getValue() const final { @@ -75,10 +75,14 @@ public: void reset() final { _acc.reset(); - _memUsageBytes = _acc.getMemUsage(); + updateMemUsage(); } private: + void updateMemUsage() { + _memUsageBytes = sizeof(*this) + _acc.getMemUsage(); + } + AccumulatorTopBottomN<sense, single> _acc; }; |
