Problem/Motivation

Logger::ensureCappedCollection() in mongodb_watchdog runs the collStats command, only to read the capped flag of the request tracker collection.

MongoDB deprecated the collStats command in server 6.2: see its documentation. It still works on the currently supported servers (8.3, 8.0 and 7.0), but will fail once a server version removes it.

Proposed resolution

Read the flag from listCollections instead, which is not deprecated: $database->listCollections(['filter' => ['name' => $name]]), then CollectionInfo::isCapped() on the result. Both exist in the MongoDB library 2.x and 1.x, so this works with either driver.

Remaining tasks

  • MR.
  • Test against MongoDB 8.3, 8.0 and 7.0, for example in Docker containers, since CI only runs one server version.

User interface changes

None.

API changes

None.

Data model changes

None.

AI disclosure

This issue and MR are developed with AI assistance

CommentFileSizeAuthor
#5 listcollections-scaling-3626728.md13.3 KBfgm

Issue fork mongodb-3626728

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

fgm created an issue. See original summary.

fgm’s picture

Assigned: Unassigned » fgm

Status: findings so far, no decision yet on the scope of the fix.

The summary's proposal needs one change: CollectionInfo::isCapped() is itself @deprecated 1.0 "in favor of using getOptions", from library 1.21.0 through 2.5.0 and on v2.x.
getOptions()['capped'] is the non-deprecated equivalent.

How the servers behave, identical on MongoDB 8.3.11, 8.0.32 and 7.0.43 (Docker images), with mongodb/mongodb 2.5.0:

Operation Result
convertToCapped, database absent error 26 "database … not found": the reason ensureCollection() writes and deletes a dummy document
createCollection with capped, database absent works, and creates the database
createCollection again, identical options no-op
createCollection, existing collection with other options error 48 (namespace exists)
convertToCapped, existing uncapped collection works
collMod cappedSize / cappedMax works
collStats, absent collection returns ok with zero counts

Callers of Logger::ensureSchema(), which must keep checking before acting, since the usual case is a tracker that is already correct:

  • hook_install(), and ClearConfirmForm after dropping the database: tracker and database absent.
  • hook_requirements(), in the runtime and update phases: tracker usually present and capped.

Possible approach, not decided:

  • One listCollections with a name filter gives existence and options, replacing listCollections plus collStats.
  • Absent: create the collection capped directly, which needs no dummy write.
  • Present but uncapped: convertToCapped, as now. Capped: nothing, or collMod if the configured size changed.
  • Error 48 on create, from a concurrent writer: read again and apply the matching case.

ListCollections is a known problem at scale on large sites with incorrect code logging content with dynamic messages: the methods is slow on a quiet server, but servers at scale are not normally quiet and that can take very long and slow down sites massively, or at least it did on versions < 4.2.

Related problems found on the way, scope to decide:

  • ensureCollection() passes its write concern inside the dummy document, as a field, instead of as an option: it was never applied. It goes away with the dummy write.
  • Event collections: a concurrent first event for a new template can create its collection uncapped through insertOne(), and the createCollection() in log() then throws error 48 from the logger.
  • MongodbWatchdogRequirements::check() calls ensureSchema() before the requirement checks, so a connection or capping failure throws instead of being reported, like #3627745: Fix multiple install requirement errors.
  • Nothing in the test suite covers capping.
jerome-mongodb’s picture

I reviewed the analysis against the current code, and the findings hold up. A few confirmations and one update.

On isCapped(): the deprecation is PHPDoc only. No runtime deprecation notice is emitted, and the method is still present and functional on both 1.21 and 2.x. It was added in 1.9.0 by PHPLIB-675 as an API surface decision (prefer the generic getOptions() over per-option helpers), not because the method is broken or unsafe. I opened PHPLIB-1970 to propose removing that deprecation, so the module could keep using isCapped() if it wants. The two tracks are independent though: this issue is about the server side collStats command, which is deprecated since 6.2.

On the approach: reading options.capped from a name filtered listCollections is the right replacement, and it is the path described by the Enumerating Collections spec. One note on the performance caveat in #2: it does not really apply here. ensureCollection() already runs a name filtered listCollections on every ensureSchema(), so the change does not add a new call. It removes the collStats command and reuses the existing result, so the net effect is one command less, not one more.

I can also confirm two of the related problems:

- ensureCollection() passes the write concern as a field of the dummy document instead of an option, so it was never applied. It goes away with the dummy write.

- The event collection race is real. In log(), createCollection() (Logger.php:392) is not protected, and a concurrent request that did not win the template upsert goes straight to insertOne() (Logger.php:424). That insert can create the collection uncapped first, and the concurrent createCollection() then fails with error 48.

On scope, I would keep the event collection race and the requirements throwing (#3627745) as separate issues. For the collStats removal itself, there is no announced server removal timeline, the command is still in the 8.3 manual, so the practical motivation today is the deprecation warning written to the server log on every call, not an imminent break.

fgm’s picture

fgm’s picture

StatusFileSize
new13.3 KB

To check the performance caveat in my earlier comment, I measured listCollections in a database shaped like the logger's, on MongoDB 8.3, 4.4, 4.2 and 4.0 (WiredTiger and MMAPv1), at rest and under read and read/write load. Conclusions, results and method in the attached file.

  • The exact-name listing that ensureCollection() runs stays under 1 ms p50 on every version and engine, up to 305,000 collections on 8.3, and does not slow writes even on 4.0.
  • The scanning listings, by regex or unfiltered, grow linearly with the collection count, and on 4.0 they stall collection creation and writes under load. From 4.2 on, they no longer do. This is likely the slowdown I remembered, and it comes from those listings, not from the exact-name check.
  • Only sanitycheck and eventCollections() run scanning listings:
    see #3628819: Withdraw Logger::eventCollections from public API and #3628822: Add a repair template collection mechanism.

So jerome-mongodb is right: reading options.capped from the name-filtered listing costs nothing measurable, and removing collStats leaves one command less.