SERVER-120247 Add test coverage for collStats on timeseries collections during FCV upgrade/downgrade (#49534)

GitOrigin-RevId: 3a81fd59de3ba33796fc5873d8502a42bd6cb603
This commit is contained in:
Tommaso Tocci 2026-03-17 22:33:00 +01:00 committed by MongoDB Bot
parent 94b1b630ca
commit 0201e895be
7 changed files with 39 additions and 40 deletions

View File

@ -118,7 +118,6 @@ selector:
- requires_getmore
- requires_non_retryable_writes
- requires_multi_updates
- requires_collstats
roots:
- jstests/core/**/*.js
- jstests/fle2/**/*.js

View File

@ -252,7 +252,6 @@ selector:
- requires_getmore
- requires_non_retryable_writes
- requires_multi_updates
- requires_collstats
roots:
- jstests/core/**/*.js
- jstests/core_sharding/**/*.js

View File

@ -138,7 +138,6 @@ selector:
- requires_getmore
- requires_non_retryable_writes
- requires_multi_updates
- requires_collstats
roots:
- jstests/core/**/*.js
- jstests/fle2/**/*.js

View File

@ -40,6 +40,3 @@
# they fail with InterruptedDueToTimeseriesUpgradeDowngrade error.
# TODO SPM-1153 remove this tag
- requires_multi_updates
# Collstats could miss timeseries section when executed concurrently with timeseries upgrade/downgrade
# TODO SERVER-120247 remove this tag once the bug is fixed
- requires_collstats

View File

@ -123,6 +123,25 @@ export const $config = (function () {
const count = withRetryOnTimeseriesUpgradeDowngradeError(() => coll.countDocuments({}));
assert.eq(count, expectedDocs.length);
},
collStatsCmd: function (db, collName) {
const coll = getCollection(db, Random.randInt(numCollections));
const result = withRetryOnTimeseriesUpgradeDowngradeError(() =>
assert.commandWorked(db.runCommand({collStats: coll.getName()})),
);
assert.hasFields(result, ["timeseries"]);
},
collStatsAgg: function (db, collName) {
const coll = getCollection(db, Random.randInt(numCollections));
const result = withRetryOnTimeseriesUpgradeDowngradeError(() =>
coll.aggregate([{$collStats: {storageStats: {}}}]).toArray(),
);
assert.hasFields(result[0], ["storageStats"]);
assert.hasFields(result[0].storageStats, ["timeseries"]);
},
};
const setup = function (db, collName, cluster) {

View File

@ -176,6 +176,7 @@ mongo_cc_library(
"//src/mongo/db/shard_role",
"//src/mongo/db/shard_role/shard_catalog:collection_options",
"//src/mongo/db/shard_role/shard_catalog:index_catalog",
"//src/mongo/db/timeseries:catalog_helper",
"//src/mongo/db/timeseries/bucket_catalog",
"//src/mongo/util/concurrency:spin_lock",
],

View File

@ -52,6 +52,7 @@
#include "mongo/db/storage/record_store.h"
#include "mongo/db/timeseries/bucket_catalog/bucket_catalog.h"
#include "mongo/db/timeseries/bucket_catalog/global_bucket_catalog.h"
#include "mongo/db/timeseries/catalog_helper.h"
#include "mongo/db/topology/cluster_role.h"
#include "mongo/logv2/log.h"
#include "mongo/stdx/unordered_map.h"
@ -319,40 +320,32 @@ Status appendCollectionStorageStats(OperationContext* opCtx,
bool numericOnly = storageStatsSpec.getNumericOnly();
static constexpr auto kStorageStatsField = "storageStats"_sd;
// TODO(SERVER-110087): Remove this legacy timeseries translation logic once v9.0 is last LTS
const auto bucketNss =
nss.isTimeseriesBucketsCollection() ? nss : nss.makeTimeseriesBucketsNamespace();
// Hold reference to the catalog for collection lookup without locks to be safe.
auto catalog = CollectionCatalog::get(opCtx);
auto bucketsColl = catalog->lookupCollectionByNamespace(opCtx, bucketNss);
const bool mayBeLegacyTimeseries = bucketsColl && bucketsColl->getTimeseriesOptions();
const auto collNss = (mayBeLegacyTimeseries && !nss.isTimeseriesBucketsCollection())
? std::move(bucketNss)
: nss;
auto failed = [&](const DBException& ex) {
LOGV2_DEBUG(3088801,
2,
"Failed to retrieve storage statistics",
logAttrs(collNss),
"error"_attr = ex);
LOGV2_DEBUG(
3088801, 2, "Failed to retrieve storage statistics", logAttrs(nss), "error"_attr = ex);
return Status::OK();
};
boost::optional<CollectionAcquisition> collectionAcquisition;
try {
collectionAcquisition = acquireCollectionMaybeLockFree(
opCtx,
CollectionAcquisitionRequest::fromOpCtx(opCtx,
collNss,
AcquisitionPrerequisites::kRead,
waitForLock ? Date_t::max() : Date_t::now()));
// TODO(SERVER-110087): switch to acquireCollection once v9.0 is last LTS
collectionAcquisition = timeseries::acquireCollectionWithBucketsLookup(
opCtx,
CollectionAcquisitionRequest::fromOpCtx(
opCtx,
nss,
AcquisitionPrerequisites::kRead,
waitForLock ? Date_t::max() : Date_t::now()),
MODE_IS)
.first;
} catch (const ExceptionFor<ErrorCodes::LockTimeout>& ex) {
return failed(ex);
} catch (const ExceptionFor<ErrorCodes::MaxTimeMSExpired>& ex) {
return failed(ex);
}
const auto& collNss = collectionAcquisition->nss();
AutoStatsTracker statsTracker(opCtx,
collNss,
Top::LockType::ReadLocked,
@ -360,17 +353,7 @@ Status appendCollectionStorageStats(OperationContext* opCtx,
DatabaseProfileSettings::get(opCtx->getServiceContext())
.getDatabaseProfileLevel(collNss.dbName()));
const auto& collectionPtr =
collectionAcquisition->getCollectionPtr(); // Will be set if present
const bool isTimeseries = collectionPtr && collectionPtr->getTimeseriesOptions().has_value();
// We decided the requested namespace was a time series view, so we redirected to the underlying
// buckets collection. However, when we tried to acquire that collection, it did not exist or it
// did not have time series options, which means it was dropped and potentially recreated in
// between the two calls. Logically, the collection that we were looking for does not exist.
bool logicallyNotFound = collNss != nss && !isTimeseries;
if (!collectionPtr || logicallyNotFound) {
if (!collectionAcquisition->exists()) {
result->appendNumber("size", 0);
result->appendNumber("count", 0);
result->appendNumber("numOrphanDocs", 0);
@ -385,6 +368,8 @@ Status appendCollectionStorageStats(OperationContext* opCtx,
"Collection [" + collNss.toStringForErrorMsg() + "] not found."};
}
const auto& collectionPtr = collectionAcquisition->getCollectionPtr();
// We will parse all 'filterObj' into different groups of data to compute. This groups will be
// marked and appended to the vector 'groupsToCompute'. In addition, if the filterObj doesn't
// exist (filterObj == boost::none), we will retrieve all stats for all fields.
@ -421,7 +406,7 @@ Status appendCollectionStorageStats(OperationContext* opCtx,
serializationCtx,
nss.isNamespaceAlwaysUntracked(),
scale,
isTimeseries,
collectionPtr->isTimeseriesCollection(),
result);
break;
case StorageStatsGroups::kRecordStoreField: