maxEstimatedScanBytes rejected/rejectedAndOverridden/dryRunWouldReject metrics overcounted for rooted $or queries

XMLWordPrintableJSON

    • Type: Task
    • Resolution: Fixed
    • Priority: Major - P3
    • 9.1.0-rc0
    • Affects Version/s: None
    • Component/s: None
    • None
    • Query Execution
    • Fully Compatible
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Overview

      The query.maxEstimatedScanBytes.rejected, rejectedAndOverridden, and dryRunWouldReject (SERVER-130231) FTDC counters can be incremented more than once for what is logically a single rejected user query, when the query has a rooted $or that goes through classic subplanning.

      Mechanism

      SubplanStage::pickBestPlan (src/mongo/db/exec/classic/subplan.cpp) calls QueryPlanner::planSubqueries, which calls QueryPlanner::plan() once per $or branch (query_planner.cpp, around line 2439). The branch loop returns immediately (with a non-OK status) on the first branch that fails to plan – e.g. because maxEstimatedScanBytes rejects it. This makes subplanningStatus non-OK, which then triggers a fallback call to SubplanStage::choosePlanWholeQuery, which calls QueryPlanner::plan() again, this time on the whole (un-split) query. If the collection also exceeds the threshold for the whole-query plan (which it typically will, since it's the same collection), this second call also triggers rejection and increments the counter again.

      Reproduction

      Verified empirically against a build from the kyle.burgess/SERVER-130231 branch:

      db.heavy.insertMany(...); // 200 docs, no indexes, collection larger than threshold
      db.adminCommand({setParameter: 1, maxEstimatedScanBytes: heavySize - 1});
      const before = db.adminCommand({serverStatus: 1}).metrics.query.maxEstimatedScanBytes.rejected;
      db.runCommand({find: "heavy", filter: {$or: [{a: 5}, {b: 7}]}}); // rejected, no index on a or b
      const after = db.adminCommand({serverStatus: 1}).metrics.query.maxEstimatedScanBytes.rejected;
      // before=0, after=2 -- one rejected $or query incremented the counter by 2, not 1.
      

      Impact

      This is an observability/metrics-accuracy issue, not a correctness issue – the query is still correctly rejected (or, for SERVER-130231's dry-run mode, correctly allowed through with a log line). But operators using these counters to gauge production impact (e.g. before enabling maxEstimatedScanBytes enforcement via dry-run mode) will see inflated counts for any workload with rooted $or queries lacking indexes on all branches, which could distort capacity-planning decisions.

      Suggested fix direction

      Consider de-duplicating the increment per top-level CanonicalQuery (e.g. via a flag/counter check higher up the call stack, before subplanning branches out), rather than incrementing at each low-level QueryPlanner::plan() call site. This would need to account for: the branch-loop-then-whole-query-fallback pattern described above, and the already-known per-outer-document replanning under classic NLJ for $lookup (a separate, arguably-intentional multiplicity noted in SERVER-127688/SERVER-130231 test comments).

      Related

      • SERVER-127688 (introduced maxEstimatedScanBytes and these counters)
      • SERVER-130231 (dry-run mode; inherits the same overcounting for dryRunWouldReject)

            Assignee:
            Kyle Burgess
            Reporter:
            Kyle Burgess
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: