1 
2 /**
3  *    Copyright (C) 2018-present MongoDB, Inc.
4  *
5  *    This program is free software: you can redistribute it and/or modify
6  *    it under the terms of the Server Side Public License, version 1,
7  *    as published by MongoDB, Inc.
8  *
9  *    This program is distributed in the hope that it will be useful,
10  *    but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *    Server Side Public License for more details.
13  *
14  *    You should have received a copy of the Server Side Public License
15  *    along with this program. If not, see
16  *    <http://www.mongodb.com/licensing/server-side-public-license>.
17  *
18  *    As a special exception, the copyright holders give permission to link the
19  *    code of portions of this program with the OpenSSL library under certain
20  *    conditions as described in each individual source file and distribute
21  *    linked combinations including the program with the OpenSSL library. You
22  *    must comply with the Server Side Public License in all respects for
23  *    all of the code used other than as permitted herein. If you modify file(s)
24  *    with this exception, you may extend this exception to your version of the
25  *    file(s), but you are not obligated to do so. If you do not wish to do so,
26  *    delete this exception statement from your version. If you delete this
27  *    exception statement from all source files in the program, then also delete
28  *    it in the license file.
29  */
30 
31 // CHECK_LOG_REDACTION
32 
33 #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kDefault
34 
35 #include "mongo/platform/basic.h"
36 
37 #include "mongo/db/curop.h"
38 
39 #include "mongo/base/disallow_copying.h"
40 #include "mongo/bson/mutable/document.h"
41 #include "mongo/db/client.h"
42 #include "mongo/db/commands.h"
43 #include "mongo/db/commands/server_status_metric.h"
44 #include "mongo/db/json.h"
45 #include "mongo/db/query/getmore_request.h"
46 #include "mongo/db/query/plan_summary_stats.h"
47 #include "mongo/rpc/metadata/client_metadata.h"
48 #include "mongo/rpc/metadata/client_metadata_ismaster.h"
49 #include "mongo/util/log.h"
50 #include "mongo/util/stringutils.h"
51 
52 namespace mongo {
53 
54 using std::string;
55 
56 namespace {
57 
58 // Lists the $-prefixed query options that can be passed alongside a wrapped query predicate for
59 // OP_QUERY find. The $orderby field is omitted because "orderby" (no dollar sign) is also allowed,
60 // and this requires special handling.
61 const std::vector<const char*> kDollarQueryModifiers = {
62     "$hint",
63     "$comment",
64     "$maxScan",
65     "$max",
66     "$min",
67     "$returnKey",
68     "$showDiskLoc",
69     "$snapshot",
70     "$maxTimeMS",
71 };
72 
73 }  // namespace
74 
upconvertQueryEntry(const BSONObj & query,const NamespaceString & nss,int ntoreturn,int ntoskip)75 BSONObj upconvertQueryEntry(const BSONObj& query,
76                             const NamespaceString& nss,
77                             int ntoreturn,
78                             int ntoskip) {
79     BSONObjBuilder bob;
80 
81     bob.append("find", nss.coll());
82 
83     // Whether or not the query predicate is wrapped inside a "query" or "$query" field so that
84     // other options can be passed alongside the predicate.
85     bool predicateIsWrapped = false;
86 
87     // Extract the query predicate.
88     BSONObj filter;
89     if (query["query"].isABSONObj()) {
90         predicateIsWrapped = true;
91         bob.appendAs(query["query"], "filter");
92     } else if (query["$query"].isABSONObj()) {
93         predicateIsWrapped = true;
94         bob.appendAs(query["$query"], "filter");
95     } else if (!query.isEmpty()) {
96         bob.append("filter", query);
97     }
98 
99     if (ntoskip) {
100         bob.append("skip", ntoskip);
101     }
102     if (ntoreturn) {
103         bob.append("ntoreturn", ntoreturn);
104     }
105 
106     // The remainder of the query options are only available if the predicate is passed in wrapped
107     // form. If the predicate is not wrapped, we're done.
108     if (!predicateIsWrapped) {
109         return bob.obj();
110     }
111 
112     // Extract the sort.
113     if (auto elem = query["orderby"]) {
114         bob.appendAs(elem, "sort");
115     } else if (auto elem = query["$orderby"]) {
116         bob.appendAs(elem, "sort");
117     }
118 
119     // Add $-prefixed OP_QUERY modifiers, like $hint.
120     for (auto modifier : kDollarQueryModifiers) {
121         if (auto elem = query[modifier]) {
122             // Use "+ 1" to omit the leading dollar sign.
123             bob.appendAs(elem, modifier + 1);
124         }
125     }
126 
127     return bob.obj();
128 }
129 
upconvertGetMoreEntry(const NamespaceString & nss,CursorId cursorId,int ntoreturn)130 BSONObj upconvertGetMoreEntry(const NamespaceString& nss, CursorId cursorId, int ntoreturn) {
131     return GetMoreRequest(nss,
132                           cursorId,
133                           ntoreturn,
134                           boost::none,  // awaitDataTimeout
135                           boost::none,  // term
136                           boost::none   // lastKnownCommittedOpTime
137                           )
138         .toBSON();
139 }
140 
141 /**
142  * This type decorates a Client object with a stack of active CurOp objects.
143  *
144  * It encapsulates the nesting logic for curops attached to a Client, along with
145  * the notion that there is always a root CurOp attached to a Client.
146  *
147  * The stack itself is represented in the _parent pointers of the CurOp class.
148  */
149 class CurOp::CurOpStack {
150     MONGO_DISALLOW_COPYING(CurOpStack);
151 
152 public:
CurOpStack()153     CurOpStack() : _base(nullptr, this) {}
154 
155     /**
156      * Returns the top of the CurOp stack.
157      */
top() const158     CurOp* top() const {
159         return _top;
160     }
161 
162     /**
163      * Adds "curOp" to the top of the CurOp stack for a client. Called by CurOp's constructor.
164      */
push(OperationContext * opCtx,CurOp * curOp)165     void push(OperationContext* opCtx, CurOp* curOp) {
166         invariant(opCtx);
167         if (_opCtx) {
168             invariant(_opCtx == opCtx);
169         } else {
170             _opCtx = opCtx;
171         }
172         stdx::lock_guard<Client> lk(*_opCtx->getClient());
173         push_nolock(curOp);
174     }
175 
push_nolock(CurOp * curOp)176     void push_nolock(CurOp* curOp) {
177         invariant(!curOp->_parent);
178         curOp->_parent = _top;
179         _top = curOp;
180     }
181 
182     /**
183      * Pops the top off the CurOp stack for a Client. Called by CurOp's destructor.
184      */
pop()185     CurOp* pop() {
186         // It is not necessary to lock when popping the final item off of the curop stack. This
187         // is because the item at the base of the stack is owned by the stack itself, and is not
188         // popped until the stack is being destroyed.  By the time the stack is being destroyed,
189         // no other threads can be observing the Client that owns the stack, because it has been
190         // removed from its ServiceContext's set of owned clients.  Further, because the last
191         // item is popped in the destructor of the stack, and that destructor runs during
192         // destruction of the owning client, it is not safe to access other member variables of
193         // the client during the final pop.
194         const bool shouldLock = _top->_parent;
195         if (shouldLock) {
196             invariant(_opCtx);
197             _opCtx->getClient()->lock();
198         }
199         invariant(_top);
200         CurOp* retval = _top;
201         _top = _top->_parent;
202         if (shouldLock) {
203             _opCtx->getClient()->unlock();
204         }
205         return retval;
206     }
207 
208 private:
209     OperationContext* _opCtx = nullptr;
210 
211     // Top of the stack of CurOps for a Client.
212     CurOp* _top = nullptr;
213 
214     // The bottom-most CurOp for a client.
215     const CurOp _base;
216 };
217 
218 const OperationContext::Decoration<CurOp::CurOpStack> CurOp::_curopStack =
219     OperationContext::declareDecoration<CurOp::CurOpStack>();
220 
get(const OperationContext * opCtx)221 CurOp* CurOp::get(const OperationContext* opCtx) {
222     return get(*opCtx);
223 }
224 
get(const OperationContext & opCtx)225 CurOp* CurOp::get(const OperationContext& opCtx) {
226     return _curopStack(opCtx).top();
227 }
228 
CurOp(OperationContext * opCtx)229 CurOp::CurOp(OperationContext* opCtx) : CurOp(opCtx, &_curopStack(opCtx)) {
230     // If this is a sub-operation, we store the snapshot of lock stats as the base lock stats of the
231     // current operation.
232     if (_parent != nullptr) {
233         Locker::LockerInfo info;
234         opCtx->lockState()->getLockerInfo(&info, boost::none);
235         _lockStatsBase = info.stats;
236     }
237 }
238 
CurOp(OperationContext * opCtx,CurOpStack * stack)239 CurOp::CurOp(OperationContext* opCtx, CurOpStack* stack) : _stack(stack) {
240     if (opCtx) {
241         _stack->push(opCtx, this);
242     } else {
243         _stack->push_nolock(this);
244     }
245 }
246 
setMessage_inlock(const char * msg,std::string name,unsigned long long progressMeterTotal,int secondsBetween)247 ProgressMeter& CurOp::setMessage_inlock(const char* msg,
248                                         std::string name,
249                                         unsigned long long progressMeterTotal,
250                                         int secondsBetween) {
251     if (progressMeterTotal) {
252         if (_progressMeter.isActive()) {
253             error() << "old _message: " << redact(_message) << " new message:" << redact(msg);
254             verify(!_progressMeter.isActive());
255         }
256         _progressMeter.reset(progressMeterTotal, secondsBetween);
257         _progressMeter.setName(name);
258     } else {
259         _progressMeter.finished();
260     }
261     _message = msg;
262     return _progressMeter;
263 }
264 
~CurOp()265 CurOp::~CurOp() {
266     invariant(this == _stack->pop());
267 }
268 
setNS_inlock(StringData ns)269 void CurOp::setNS_inlock(StringData ns) {
270     _ns = ns.toString();
271 }
272 
ensureStarted()273 void CurOp::ensureStarted() {
274     if (_start == 0) {
275         _start = curTimeMicros64();
276     }
277 }
278 
enter_inlock(const char * ns,boost::optional<int> dbProfileLevel)279 void CurOp::enter_inlock(const char* ns, boost::optional<int> dbProfileLevel) {
280     ensureStarted();
281     _ns = ns;
282     if (dbProfileLevel) {
283         raiseDbProfileLevel(*dbProfileLevel);
284     }
285 }
286 
raiseDbProfileLevel(int dbProfileLevel)287 void CurOp::raiseDbProfileLevel(int dbProfileLevel) {
288     _dbprofile = std::max(dbProfileLevel, _dbprofile);
289 }
290 
getReadWriteType() const291 Command::ReadWriteType CurOp::getReadWriteType() const {
292     if (_command) {
293         return _command->getReadWriteType();
294     }
295     switch (_logicalOp) {
296         case LogicalOp::opGetMore:
297         case LogicalOp::opQuery:
298             return Command::ReadWriteType::kRead;
299         case LogicalOp::opUpdate:
300         case LogicalOp::opInsert:
301         case LogicalOp::opDelete:
302             return Command::ReadWriteType::kWrite;
303         default:
304             return Command::ReadWriteType::kCommand;
305     }
306 }
307 
308 namespace {
309 
310 /**
311  * Appends {<name>: obj} to the provided builder.  If obj is greater than maxSize, appends a string
312  * summary of obj as { <name>: { $truncated: "obj" } }. If a comment parameter is present, add it to
313  * the truncation object.
314  */
appendAsObjOrString(StringData name,const BSONObj & obj,const boost::optional<size_t> maxSize,BSONObjBuilder * builder)315 void appendAsObjOrString(StringData name,
316                          const BSONObj& obj,
317                          const boost::optional<size_t> maxSize,
318                          BSONObjBuilder* builder) {
319     if (!maxSize || static_cast<size_t>(obj.objsize()) <= *maxSize) {
320         builder->append(name, obj);
321     } else {
322         // Generate an abbreviated serialization for the object, by passing false as the
323         // "full" argument to obj.toString().
324         std::string objToString = obj.toString();
325         if (objToString.size() > *maxSize) {
326             // objToString is still too long, so we append to the builder a truncated form
327             // of objToString concatenated with "...".  Instead of creating a new string
328             // temporary, mutate objToString to do this (we know that we can mutate
329             // characters in objToString up to and including objToString[maxSize]).
330             objToString[*maxSize - 3] = '.';
331             objToString[*maxSize - 2] = '.';
332             objToString[*maxSize - 1] = '.';
333         }
334 
335         StringData truncation = StringData(objToString).substr(0, *maxSize);
336 
337         // Append the truncated representation of the object to the builder. If a comment parameter
338         // is present, write it to the object alongside the truncated op. This object will appear as
339         // {$truncated: "{find: \"collection\", filter: {x: 1, ...", comment: "comment text" }
340         BSONObjBuilder truncatedBuilder(builder->subobjStart(name));
341         truncatedBuilder.append("$truncated", truncation);
342 
343         if (auto comment = obj["comment"]) {
344             truncatedBuilder.append(comment);
345         }
346 
347         truncatedBuilder.doneFast();
348     }
349 }
350 }  // namespace
351 
reportState(BSONObjBuilder * builder,bool truncateOps)352 void CurOp::reportState(BSONObjBuilder* builder, bool truncateOps) {
353     if (_start) {
354         builder->append("secs_running", durationCount<Seconds>(elapsedTimeTotal()));
355         builder->append("microsecs_running", durationCount<Microseconds>(elapsedTimeTotal()));
356     }
357 
358     builder->append("op", logicalOpToString(_logicalOp));
359     builder->append("ns", _ns);
360 
361     // When the currentOp command is run, it returns a single response object containing all current
362     // operations; this request will fail if the response exceeds the 16MB document limit. By
363     // contrast, the $currentOp aggregation stage does not have this restriction. If 'truncateOps'
364     // is true, limit the size of each op to 1000 bytes. Otherwise, do not truncate.
365     const boost::optional<size_t> maxQuerySize{truncateOps, 1000};
366 
367     if (!_command && _networkOp == dbQuery) {
368         // This is a legacy OP_QUERY. We upconvert the "query" field of the currentOp output to look
369         // similar to a find command.
370         //
371         // CurOp doesn't have access to the ntoreturn or ntoskip values. By setting them to zero, we
372         // will omit mention of them in the currentOp output.
373         const int ntoreturn = 0;
374         const int ntoskip = 0;
375 
376         appendAsObjOrString(
377             "command",
378             upconvertQueryEntry(_opDescription, NamespaceString(_ns), ntoreturn, ntoskip),
379             maxQuerySize,
380             builder);
381     } else {
382         appendAsObjOrString("command", _opDescription, maxQuerySize, builder);
383     }
384 
385     if (!_originatingCommand.isEmpty()) {
386         appendAsObjOrString("originatingCommand", _originatingCommand, maxQuerySize, builder);
387     }
388 
389     if (!_planSummary.empty()) {
390         builder->append("planSummary", _planSummary);
391     }
392 
393     if (!_message.empty()) {
394         if (_progressMeter.isActive()) {
395             StringBuilder buf;
396             buf << _message << " " << _progressMeter.toString();
397             builder->append("msg", buf.str());
398             BSONObjBuilder sub(builder->subobjStart("progress"));
399             sub.appendNumber("done", (long long)_progressMeter.done());
400             sub.appendNumber("total", (long long)_progressMeter.total());
401             sub.done();
402         } else {
403             builder->append("msg", _message);
404         }
405     }
406 
407     builder->append("numYields", _numYields);
408 }
409 
410 namespace {
getProtoString(int op)411 StringData getProtoString(int op) {
412     if (op == dbMsg) {
413         return "op_msg";
414     } else if (op == dbQuery) {
415         return "op_query";
416     } else if (op == dbCommand) {
417         return "op_command";
418     }
419     MONGO_UNREACHABLE;
420 }
421 }  // namespace
422 
423 #define OPDEBUG_TOSTRING_HELP(x) \
424     if (x >= 0)                  \
425     s << " " #x ":" << (x)
426 #define OPDEBUG_TOSTRING_HELP_BOOL(x) \
427     if (x)                            \
428     s << " " #x ":" << (x)
429 
report(Client * client,const CurOp & curop,const SingleThreadedLockStats & lockStats) const430 string OpDebug::report(Client* client,
431                        const CurOp& curop,
432                        const SingleThreadedLockStats& lockStats) const {
433     StringBuilder s;
434     if (iscommand)
435         s << "command ";
436     else
437         s << networkOpToString(networkOp) << ' ';
438 
439     s << curop.getNS();
440 
441     const auto& clientMetadata = ClientMetadataIsMasterState::get(client).getClientMetadata();
442     if (clientMetadata) {
443         auto appName = clientMetadata.get().getApplicationName();
444         if (!appName.empty()) {
445             s << " appName: \"" << escape(appName) << '\"';
446         }
447     }
448 
449     BSONObj query;
450 
451     // If necessary, upconvert legacy find operations so that their log lines resemble their find
452     // command counterpart.
453     if (!iscommand && networkOp == dbQuery) {
454         query = upconvertQueryEntry(
455             curop.opDescription(), NamespaceString(curop.getNS()), ntoreturn, ntoskip);
456     } else {
457         query = curop.opDescription();
458     }
459 
460     if (!query.isEmpty()) {
461         s << " command: ";
462         if (iscommand) {
463             Command* curCommand = curop.getCommand();
464             if (curCommand) {
465                 mutablebson::Document cmdToLog(query, mutablebson::Document::kInPlaceDisabled);
466                 curCommand->redactForLogging(&cmdToLog);
467                 s << curCommand->getName() << " ";
468                 s << redact(cmdToLog.getObject());
469             } else {  // Should not happen but we need to handle curCommand == NULL gracefully.
470                 s << redact(query);
471             }
472         } else {
473             s << redact(query);
474         }
475     }
476 
477     auto originatingCommand = curop.originatingCommand();
478     if (!originatingCommand.isEmpty()) {
479         s << " originatingCommand: " << redact(originatingCommand);
480     }
481 
482     if (!curop.getPlanSummary().empty()) {
483         s << " planSummary: " << curop.getPlanSummary().toString();
484     }
485 
486     OPDEBUG_TOSTRING_HELP(cursorid);
487     OPDEBUG_TOSTRING_HELP(ntoreturn);
488     OPDEBUG_TOSTRING_HELP(ntoskip);
489     OPDEBUG_TOSTRING_HELP_BOOL(exhaust);
490 
491     OPDEBUG_TOSTRING_HELP(keysExamined);
492     OPDEBUG_TOSTRING_HELP(docsExamined);
493     OPDEBUG_TOSTRING_HELP_BOOL(hasSortStage);
494     OPDEBUG_TOSTRING_HELP_BOOL(fromMultiPlanner);
495     if (replanReason) {
496         bool replanned = true;
497         OPDEBUG_TOSTRING_HELP_BOOL(replanned);
498         s << " replanReason:\"" << escape(redact(*replanReason)) << "\"";
499     }
500     OPDEBUG_TOSTRING_HELP(nMatched);
501     OPDEBUG_TOSTRING_HELP(nModified);
502     OPDEBUG_TOSTRING_HELP(ninserted);
503     OPDEBUG_TOSTRING_HELP(ndeleted);
504     OPDEBUG_TOSTRING_HELP_BOOL(fastmodinsert);
505     OPDEBUG_TOSTRING_HELP_BOOL(upsert);
506     OPDEBUG_TOSTRING_HELP_BOOL(cursorExhausted);
507 
508     if (nmoved > 0) {
509         s << " nmoved:" << nmoved;
510     }
511 
512     if (keysInserted > 0) {
513         s << " keysInserted:" << keysInserted;
514     }
515 
516     if (keysDeleted > 0) {
517         s << " keysDeleted:" << keysDeleted;
518     }
519 
520     if (writeConflicts > 0) {
521         s << " writeConflicts:" << writeConflicts;
522     }
523 
524     if (!exceptionInfo.isOK()) {
525         s << " exception: " << redact(exceptionInfo.reason());
526         s << " code:" << exceptionInfo.code();
527     }
528 
529     s << " numYields:" << curop.numYields();
530 
531     OPDEBUG_TOSTRING_HELP(nreturned);
532     if (responseLength > 0) {
533         s << " reslen:" << responseLength;
534     }
535 
536     {
537         BSONObjBuilder locks;
538         lockStats.report(&locks);
539         s << " locks:" << locks.obj().toString();
540     }
541 
542     if (iscommand) {
543         s << " protocol:" << getProtoString(networkOp);
544     }
545 
546     s << " " << (executionTimeMicros / 1000) << "ms";
547 
548     return s.str();
549 }
550 
551 #define OPDEBUG_APPEND_NUMBER(x) \
552     if (x != -1)                 \
553     b.appendNumber(#x, (x))
554 #define OPDEBUG_APPEND_BOOL(x) \
555     if (x)                     \
556     b.appendBool(#x, (x))
557 
append(const CurOp & curop,const SingleThreadedLockStats & lockStats,BSONObjBuilder & b) const558 void OpDebug::append(const CurOp& curop,
559                      const SingleThreadedLockStats& lockStats,
560                      BSONObjBuilder& b) const {
561     const size_t maxElementSize = 50 * 1024;
562 
563     b.append("op", logicalOpToString(logicalOp));
564 
565     NamespaceString nss = NamespaceString(curop.getNS());
566     b.append("ns", nss.ns());
567 
568     if (!iscommand && networkOp == dbQuery) {
569         appendAsObjOrString("command",
570                             upconvertQueryEntry(curop.opDescription(), nss, ntoreturn, ntoskip),
571                             maxElementSize,
572                             &b);
573     } else if (curop.haveOpDescription()) {
574         appendAsObjOrString("command", curop.opDescription(), maxElementSize, &b);
575     }
576 
577     auto originatingCommand = curop.originatingCommand();
578     if (!originatingCommand.isEmpty()) {
579         appendAsObjOrString("originatingCommand", originatingCommand, maxElementSize, &b);
580     }
581 
582     OPDEBUG_APPEND_NUMBER(cursorid);
583     OPDEBUG_APPEND_BOOL(exhaust);
584 
585     OPDEBUG_APPEND_NUMBER(keysExamined);
586     OPDEBUG_APPEND_NUMBER(docsExamined);
587     OPDEBUG_APPEND_BOOL(hasSortStage);
588     OPDEBUG_APPEND_BOOL(fromMultiPlanner);
589     if (replanReason) {
590         bool replanned = true;
591         OPDEBUG_APPEND_BOOL(replanned);
592         b.append("replanReason", *replanReason);
593     }
594     OPDEBUG_APPEND_NUMBER(nMatched);
595     OPDEBUG_APPEND_NUMBER(nModified);
596     OPDEBUG_APPEND_NUMBER(ninserted);
597     OPDEBUG_APPEND_NUMBER(ndeleted);
598     OPDEBUG_APPEND_BOOL(fastmodinsert);
599     OPDEBUG_APPEND_BOOL(upsert);
600     OPDEBUG_APPEND_BOOL(cursorExhausted);
601 
602     if (nmoved > 0) {
603         b.appendNumber("nmoved", nmoved);
604     }
605 
606     if (keysInserted > 0) {
607         b.appendNumber("keysInserted", keysInserted);
608     }
609 
610     if (keysDeleted > 0) {
611         b.appendNumber("keysDeleted", keysDeleted);
612     }
613 
614     if (writeConflicts > 0) {
615         b.appendNumber("writeConflicts", writeConflicts);
616     }
617 
618     b.appendNumber("numYield", curop.numYields());
619 
620     {
621         BSONObjBuilder locks(b.subobjStart("locks"));
622         lockStats.report(&locks);
623     }
624 
625     if (!exceptionInfo.isOK()) {
626         b.append("exception", exceptionInfo.reason());
627         b.append("exceptionCode", exceptionInfo.code());
628     }
629 
630     OPDEBUG_APPEND_NUMBER(nreturned);
631     OPDEBUG_APPEND_NUMBER(responseLength);
632     if (iscommand) {
633         b.append("protocol", getProtoString(networkOp));
634     }
635     b.appendIntOrLL("millis", executionTimeMicros / 1000);
636 
637     if (!curop.getPlanSummary().empty()) {
638         b.append("planSummary", curop.getPlanSummary());
639     }
640 
641     if (!execStats.isEmpty()) {
642         b.append("execStats", execStats);
643     }
644 }
645 
setPlanSummaryMetrics(const PlanSummaryStats & planSummaryStats)646 void OpDebug::setPlanSummaryMetrics(const PlanSummaryStats& planSummaryStats) {
647     keysExamined = planSummaryStats.totalKeysExamined;
648     docsExamined = planSummaryStats.totalDocsExamined;
649     hasSortStage = planSummaryStats.hasSortStage;
650     fromMultiPlanner = planSummaryStats.fromMultiPlanner;
651     replanReason = planSummaryStats.replanReason;
652 }
653 
654 }  // namespace mongo
655