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
| Comment | File | Size | Author |
|---|---|---|---|
| #5 | listcollections-scaling-3626728.md | 13.3 KB | fgm |
Issue fork mongodb-3626728
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
Comment #2
fgmStatus: 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 onv2.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:
convertToCapped, database absentensureCollection()writes and deletes a dummy documentcreateCollectionwithcapped, database absentcreateCollectionagain, identical optionscreateCollection, existing collection with other optionsconvertToCapped, existing uncapped collectioncollModcappedSize/cappedMaxcollStats, absent collectionokwith zero countsCallers of
Logger::ensureSchema(), which must keep checking before acting, since the usual case is a tracker that is already correct:hook_install(), andClearConfirmFormafter dropping the database: tracker and database absent.hook_requirements(), in theruntimeandupdatephases: tracker usually present and capped.Possible approach, not decided:
listCollectionswith a name filter gives existence and options, replacinglistCollectionspluscollStats.convertToCapped, as now. Capped: nothing, orcollModif the configured size changed.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.insertOne(), and thecreateCollection()inlog()then throws error 48 from the logger.MongodbWatchdogRequirements::check()callsensureSchema()before the requirement checks, so a connection or capping failure throws instead of being reported, like #3627745: Fix multiple install requirement errors.Comment #3
jerome-mongodb commentedI 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 genericgetOptions()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 usingisCapped()if it wants. The two tracks are independent though: this issue is about the server sidecollStatscommand, which is deprecated since 6.2.On the approach: reading
options.cappedfrom a name filteredlistCollectionsis 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 filteredlistCollectionson everyensureSchema(), so the change does not add a new call. It removes thecollStatscommand 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 toinsertOne()(Logger.php:424). That insert can create the collection uncapped first, and the concurrentcreateCollection()then fails with error 48.On scope, I would keep the event collection race and the requirements throwing (#3627745) as separate issues. For the
collStatsremoval 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.Comment #4
fgmI split the other findings to their own issues so this one can focus on this collStats:
Comment #5
fgmTo check the performance caveat in my earlier comment, I measured
listCollectionsin 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.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.sanitycheckandeventCollections()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.cappedfrom the name-filtered listing costs nothing measurable, and removingcollStatsleaves one command less.