diff options
-rw-r--r-- | jstests/change_streams/timeseries.js | 6 | ||||
-rw-r--r-- | jstests/core/timeseries/bucket_span_and_rounding_seconds.js | 243 | ||||
-rw-r--r-- | jstests/core/timeseries/timeseries_list_collections.js | 92 | ||||
-rw-r--r-- | jstests/core/timeseries/timeseries_list_collections_filter_name.js | 29 | ||||
-rw-r--r-- | jstests/noPassthrough/timeseries_create.js | 74 | ||||
-rw-r--r-- | src/mongo/db/catalog/create_collection.cpp | 30 | ||||
-rw-r--r-- | src/mongo/db/commands/write_commands.cpp | 38 | ||||
-rw-r--r-- | src/mongo/db/exec/bucket_unpacker.cpp | 3 | ||||
-rw-r--r-- | src/mongo/db/s/collmod_coordinator.cpp | 5 | ||||
-rw-r--r-- | src/mongo/db/s/shard_server_catalog_cache_loader_test.cpp | 1 | ||||
-rw-r--r-- | src/mongo/db/timeseries/timeseries.idl | 2 | ||||
-rw-r--r-- | src/mongo/db/timeseries/timeseries_options.cpp | 164 | ||||
-rw-r--r-- | src/mongo/db/timeseries/timeseries_options.h | 7 | ||||
-rw-r--r-- | src/mongo/s/catalog_cache_test.cpp | 4 | ||||
-rw-r--r-- | src/mongo/s/query/cluster_aggregate.cpp | 21 |
15 files changed, 616 insertions, 103 deletions
diff --git a/jstests/change_streams/timeseries.js b/jstests/change_streams/timeseries.js index 31984390dc9..2c08fec5253 100644 --- a/jstests/change_streams/timeseries.js +++ b/jstests/change_streams/timeseries.js @@ -291,6 +291,12 @@ let expectedChanges = [ {"operationType": "drop", "ns": {"db": dbName, "coll": bucketsCollName}} ]; +if (FeatureFlagUtil.isEnabled(db, "TimeseriesScalabilityImprovements")) { + expectedChanges[0].operationDescription.timeseries.bucketRoundingSeconds = 60; + expectedChanges[8].stateBeforeChange.collectionOptions.timeseries.bucketRoundingSeconds = 60; + expectedChanges[9].stateBeforeChange.collectionOptions.timeseries.bucketRoundingSeconds = 86400; +} + if (!FeatureFlagUtil.isEnabled(db, "TimeseriesScalabilityImprovements")) { // Under this feature flag, buckets are created with the closed field set to false. // Remove closed field if the feature flag is not enabled. diff --git a/jstests/core/timeseries/bucket_span_and_rounding_seconds.js b/jstests/core/timeseries/bucket_span_and_rounding_seconds.js new file mode 100644 index 00000000000..51687928b64 --- /dev/null +++ b/jstests/core/timeseries/bucket_span_and_rounding_seconds.js @@ -0,0 +1,243 @@ +/** + * Tests timeseries collection creation with bucketRoundingSeconds and bucketMaxSpanSeconds + * parameters and checks that we correctly set their value (failing when parameters are + * not added correctly or are missing). + * + * @tags: [ + * # "Overriding safe failed response for :: create" + * does_not_support_stepdowns, + * # We need a timeseries collection. + * requires_timeseries, + * ] + */ +(function() { +'use strict'; + +load("jstests/core/timeseries/libs/timeseries.js"); + +if (!TimeseriesTest.timeseriesScalabilityImprovementsEnabled(db.getMongo())) { + jsTestLog( + "Skipped test as the featureFlagTimeseriesScalabilityImprovements feature flag is not enabled."); + return; +} + +const testDB = db.getSiblingDB(jsTestName()); +assert.commandWorked(testDB.dropDatabase()); + +const timeFieldName = 'time'; +const coll = testDB.t; +const bucketRoundingSecondsTime = 4000; +const bucketMaxSpanSecondsTime = 4000; +const granularitySeconds = "seconds"; +const granularityMinutes = "minutes"; +const granularityHours = "hours"; +const bucketInvalidOptionsError = ErrorCodes.InvalidOptions; + +const granularityTimeOptionsArr = [granularitySeconds, granularityMinutes, granularityHours]; + +const getBucketMaxSpanSecondsFromGranularity = function(granularity) { + switch (granularity) { + case 'seconds': + return 60 * 60; + case 'minutes': + return 60 * 60 * 24; + case 'hours': + return 60 * 60 * 24 * 30; + default: + assert(false, 'Invalid granularity: ' + granularity); + } +}; + +const getBucketRoundingSecondsFromGranularity = function(granularity) { + switch (granularity) { + case 'seconds': + return 60; + case 'minutes': + return 60 * 60; + case 'hours': + return 60 * 60 * 24; + default: + assert(false, 'Invalid granularity: ' + granularity); + } +}; + +const verifyCreateCommandFails = function(secondsOptions = {}, errorCode) { + coll.drop(); + const fullTimeseriesOptions = Object.merge({timeField: timeFieldName}, secondsOptions); + + if (errorCode) { + assert.commandFailedWithCode( + testDB.createCollection(coll.getName(), {timeseries: fullTimeseriesOptions}), + errorCode); + } else { + assert.commandFailed( + testDB.createCollection(coll.getName(), {timeseries: fullTimeseriesOptions})); + } + + const collections = + assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; + + assert.isnull(collections.find(entry => entry.name === 'system.buckets.' + coll.getName())); + assert.isnull(collections.find(entry => entry.name === coll.getName())); +}; + +(function createTimeseriesCollectionWithBucketSecondsOptions() { + jsTestLog("Create timeseries collection with bucketRoundingSeconds and bucketMaxSpanSeconds."); + // Create a timeseries collection with bucketRoundingSeconds, bucketMaxSpanSeconds and + // custom parameters. ListCollection should show view and bucket collection with the added + // properties. + assert.commandWorked(testDB.createCollection(coll.getName(), { + timeseries: { + timeField: timeFieldName, + bucketRoundingSeconds: bucketRoundingSecondsTime, + bucketMaxSpanSeconds: bucketMaxSpanSecondsTime + } + })); + + let collections = + assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; + + let collectionEntry = + collections.find(entry => entry.name === 'system.buckets.' + coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, bucketRoundingSecondsTime); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, bucketMaxSpanSecondsTime); + + collectionEntry = collections.find(entry => entry.name === coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, bucketRoundingSecondsTime); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, bucketMaxSpanSecondsTime); + + // Verify the create command succeeds with bucketRoundingSeconds, bucketMaxSpanSeconds set as + // their default granularity values. + for (const granularityTime of granularityTimeOptionsArr) { + coll.drop(); + assert.commandWorked(testDB.createCollection(coll.getName(), { + timeseries: { + timeField: timeFieldName, + granularity: granularityTime, + bucketRoundingSeconds: getBucketRoundingSecondsFromGranularity(granularityTime), + bucketMaxSpanSeconds: getBucketMaxSpanSecondsFromGranularity(granularityTime) + } + })); + collections = + assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; + + collectionEntry = + collections.find(entry => entry.name === 'system.buckets.' + coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, + getBucketRoundingSecondsFromGranularity(granularityTime)); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, + getBucketMaxSpanSecondsFromGranularity(granularityTime)); + + collectionEntry = collections.find(entry => entry.name === coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, + getBucketRoundingSecondsFromGranularity(granularityTime)); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, + getBucketMaxSpanSecondsFromGranularity(granularityTime)); + } + + // Verify the create command succeeds without setting bucketRoundingSeconds and + // bucketMaxSpanSeconds. This should set their default granularity values. + for (const granularityTime of granularityTimeOptionsArr) { + coll.drop(); + assert.commandWorked(testDB.createCollection(coll.getName(), { + timeseries: { + timeField: timeFieldName, + granularity: granularityTime, + } + })); + collections = + assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; + + collectionEntry = + collections.find(entry => entry.name === 'system.buckets.' + coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, + getBucketRoundingSecondsFromGranularity(granularityTime)); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, + getBucketMaxSpanSecondsFromGranularity(granularityTime)); + + collectionEntry = collections.find(entry => entry.name === coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, + getBucketRoundingSecondsFromGranularity(granularityTime)); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, + getBucketMaxSpanSecondsFromGranularity(granularityTime)); + } + + // Verify the create command succeeds without setting any field other than timeField, this + // should set granularity as seconds and bucketRoundingSeconds and bucketMaxSpanSeconds with + // their default granularity values. + coll.drop(); + assert.commandWorked(testDB.createCollection(coll.getName(), { + timeseries: { + timeField: timeFieldName, + } + })); + collections = assert.commandWorked(testDB.runCommand({listCollections: 1})).cursor.firstBatch; + + collectionEntry = collections.find(entry => entry.name === 'system.buckets.' + coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, + getBucketRoundingSecondsFromGranularity(granularitySeconds)); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, + getBucketMaxSpanSecondsFromGranularity(granularitySeconds)); + + collectionEntry = collections.find(entry => entry.name === coll.getName()); + assert(collectionEntry); + assert.eq(collectionEntry.options.timeseries.bucketRoundingSeconds, + getBucketRoundingSecondsFromGranularity(granularitySeconds)); + assert.eq(collectionEntry.options.timeseries.bucketMaxSpanSeconds, + getBucketMaxSpanSecondsFromGranularity(granularitySeconds)); +})(); + +(function createTimeseriesCollectionWithInvalidOptions() { + jsTestLog("Create timeseries collection with missing or extra arguments."); + + // Verify the create command fails when the 'bucketRoundingSeconds' option is set but not the + // 'bucketMaxSpanSeconds' option. + verifyCreateCommandFails({bucketRoundingSeconds: bucketRoundingSecondsTime}, + bucketInvalidOptionsError); + + // Verify the create command fails when the 'bucketMaxSpanSeconds' option is set but not the + // 'bucketRoundingSeconds' option. + verifyCreateCommandFails({bucketMaxSpanSeconds: bucketMaxSpanSecondsTime}, + bucketInvalidOptionsError); + + // Verify the create command fails when the 'bucketMaxSpanSeconds' option is set but not the + // 'bucketRoundingSeconds' option (even if set to granularity default seconds value). + verifyCreateCommandFails({bucketMaxSpanSeconds: 3600}, bucketInvalidOptionsError); + + // Verify the create command fails when bucketRoundingSeconds is different than + // bucketMaxSpanSeconds. + verifyCreateCommandFails({bucketRoundingSeconds: 100, bucketMaxSpanSeconds: 50}, + bucketInvalidOptionsError); + + // Verify the create command fails when bucketRoundingSeconds or bucketMaxSpanSeconds is a + // negative value. + verifyCreateCommandFails({bucketRoundingSeconds: -1, bucketMaxSpanSeconds: -1}); + + // Verify the create command fails when granularity is set as minutes alongside + // bucketRoundingSeconds and bucketMaxSpanSeconds and they are not the default granularity + // values. + verifyCreateCommandFails({ + granularity: granularityMinutes, + bucketRoundingSeconds: bucketRoundingSecondsTime, + bucketMaxSpanSeconds: bucketMaxSpanSecondsTime + }, + bucketInvalidOptionsError); + + // Verify the create command fails when granularity is set as hours alongside + // bucketRoundingSeconds and bucketMaxSpanSeconds and they are not the default granularity + // values. + verifyCreateCommandFails({ + granularity: granularityHours, + bucketRoundingSeconds: bucketRoundingSecondsTime, + bucketMaxSpanSeconds: bucketMaxSpanSecondsTime + }, + bucketInvalidOptionsError); +})(); +})(); diff --git a/jstests/core/timeseries/timeseries_list_collections.js b/jstests/core/timeseries/timeseries_list_collections.js index ee2d5dd143e..8364dca1fd9 100644 --- a/jstests/core/timeseries/timeseries_list_collections.js +++ b/jstests/core/timeseries/timeseries_list_collections.js @@ -9,6 +9,10 @@ (function() { 'use strict'; +load("jstests/core/timeseries/libs/timeseries.js"); + +const testDB = db.getSiblingDB(jsTestName()); + const timeFieldName = 'time'; const metaFieldName = 'meta'; @@ -28,6 +32,22 @@ const getBucketMaxSpanSeconds = function(granularity) { } }; +const getBucketRoundingSeconds = function(granularity) { + switch (granularity) { + case 'seconds': + return 60; + case 'minutes': + return 60 * 60; + case 'hours': + return 60 * 60 * 24; + default: + assert(false, 'Invalid granularity: ' + granularity); + } +}; + +const bucketMaxSpanSecondsFromMinutes = getBucketMaxSpanSeconds('minutes'); +const buckeRoundingSecondsFromMinutes = getBucketRoundingSeconds('minutes'); + const testOptions = function(options) { const coll = db.getCollection(collNamePrefix + collCount++); coll.drop(); @@ -43,6 +63,13 @@ const testOptions = function(options) { options.timeseries, {bucketMaxSpanSeconds: getBucketMaxSpanSeconds(options.timeseries.granularity)}); } + if (TimeseriesTest.timeseriesScalabilityImprovementsEnabled(testDB)) { + if (!options.timeseries.hasOwnProperty('bucketRoundingSeconds')) { + Object.assign( + options.timeseries, + {bucketRoundingSeconds: getBucketRoundingSeconds(options.timeseries.granularity)}); + } + } if (options.hasOwnProperty('collation')) { Object.assign(options.collation, { @@ -78,13 +105,24 @@ testOptions({ granularity: 'minutes', } }); -testOptions({ - timeseries: { - timeField: timeFieldName, - granularity: 'minutes', - bucketMaxSpanSeconds: 60 * 60 * 24, - } -}); +if (!TimeseriesTest.timeseriesScalabilityImprovementsEnabled(testDB)) { + testOptions({ + timeseries: { + timeField: timeFieldName, + granularity: 'minutes', + bucketMaxSpanSeconds: bucketMaxSpanSecondsFromMinutes, + } + }); +} else { + testOptions({ + timeseries: { + timeField: timeFieldName, + granularity: 'minutes', + bucketMaxSpanSeconds: bucketMaxSpanSecondsFromMinutes, + bucketRoundingSeconds: buckeRoundingSecondsFromMinutes, + } + }); +} testOptions({ timeseries: { timeField: timeFieldName, @@ -104,16 +142,32 @@ testOptions({ collation: {locale: 'ja'}, }); testOptions({timeseries: {timeField: timeFieldName}, expireAfterSeconds: NumberLong(100)}); -testOptions({ - timeseries: { - timeField: timeFieldName, - metaField: metaFieldName, - granularity: 'minutes', - bucketMaxSpanSeconds: 60 * 60 * 24, - }, - storageEngine: {wiredTiger: {}}, - indexOptionDefaults: {storageEngine: {wiredTiger: {}}}, - collation: {locale: 'ja'}, - expireAfterSeconds: NumberLong(100), -}); +if (!TimeseriesTest.timeseriesScalabilityImprovementsEnabled(testDB)) { + testOptions({ + timeseries: { + timeField: timeFieldName, + metaField: metaFieldName, + granularity: 'minutes', + bucketMaxSpanSeconds: bucketMaxSpanSecondsFromMinutes, + }, + storageEngine: {wiredTiger: {}}, + indexOptionDefaults: {storageEngine: {wiredTiger: {}}}, + collation: {locale: 'ja'}, + expireAfterSeconds: NumberLong(100), + }); +} else { + testOptions({ + timeseries: { + timeField: timeFieldName, + metaField: metaFieldName, + granularity: 'minutes', + bucketMaxSpanSeconds: bucketMaxSpanSecondsFromMinutes, + bucketRoundingSeconds: buckeRoundingSecondsFromMinutes, + }, + storageEngine: {wiredTiger: {}}, + indexOptionDefaults: {storageEngine: {wiredTiger: {}}}, + collation: {locale: 'ja'}, + expireAfterSeconds: NumberLong(100), + }); +} })(); diff --git a/jstests/core/timeseries/timeseries_list_collections_filter_name.js b/jstests/core/timeseries/timeseries_list_collections_filter_name.js index fb80b08400d..547558738a1 100644 --- a/jstests/core/timeseries/timeseries_list_collections_filter_name.js +++ b/jstests/core/timeseries/timeseries_list_collections_filter_name.js @@ -10,6 +10,8 @@ (function() { 'use strict'; +load("jstests/core/timeseries/libs/timeseries.js"); + const timeFieldName = 'time'; const coll = db.timeseries_list_collections_filter_name; @@ -20,13 +22,22 @@ assert.commandWorked(db.createCollection(coll.getName(), {timeseries: {timeField const collections = assert.commandWorked(db.runCommand({listCollections: 1, filter: {name: coll.getName()}})) .cursor.firstBatch; -assert.eq(collections, [{ - name: coll.getName(), - type: 'timeseries', - options: { - timeseries: - {timeField: timeFieldName, granularity: 'seconds', bucketMaxSpanSeconds: 3600} - }, - info: {readOnly: false}, - }]); + +const timeseriesOptions = { + timeField: timeFieldName, + granularity: 'seconds' +}; +const extraBucketingParameters = + (TimeseriesTest.timeseriesScalabilityImprovementsEnabled(db.getMongo())) + ? {bucketRoundingSeconds: 60, bucketMaxSpanSeconds: 3600} + : {bucketMaxSpanSeconds: 3600}; + +const collectionOptions = [{ + name: coll.getName(), + type: 'timeseries', + options: {timeseries: Object.merge(timeseriesOptions, extraBucketingParameters)}, + info: {readOnly: false}, +}]; + +assert.eq(collections, collectionOptions); })(); diff --git a/jstests/noPassthrough/timeseries_create.js b/jstests/noPassthrough/timeseries_create.js index 71e04e23b61..34d05c59d26 100644 --- a/jstests/noPassthrough/timeseries_create.js +++ b/jstests/noPassthrough/timeseries_create.js @@ -8,11 +8,17 @@ (function() { "use strict"; +load("jstests/core/timeseries/libs/timeseries.js"); + const conn = MongoRunner.runMongod(); const dbName = jsTestName(); +const testDB = conn.getDB(dbName); let collCount = 0; +const bucketGranularityError = ErrorCodes.InvalidOptions; +const bucketMaxSpanSecondsError = ErrorCodes.InvalidOptions; + const testOptions = function(allowed, createOptions, timeseriesOptions = { @@ -26,7 +32,6 @@ const testOptions = function(allowed, // passing all the test assertions. tearDown: (testDB, collName) => {}, }) { - const testDB = conn.getDB(dbName); const collName = 'timeseries_' + collCount++; const bucketsCollName = 'system.buckets.' + collName; @@ -92,21 +97,53 @@ testValidTimeseriesOptions({timeField: "time"}); testValidTimeseriesOptions({timeField: "time", metaField: "meta"}); testValidTimeseriesOptions({timeField: "time", metaField: "meta", granularity: "seconds"}); -// A bucketMaxSpanSeconds may be provided, but only if they are the default for the granularity. -testValidTimeseriesOptions( - {timeField: "time", metaField: "meta", granularity: "seconds", bucketMaxSpanSeconds: 60 * 60}); -testValidTimeseriesOptions({ - timeField: "time", - metaField: "meta", - granularity: "minutes", - bucketMaxSpanSeconds: 60 * 60 * 24 -}); -testValidTimeseriesOptions({ - timeField: "time", - metaField: "meta", - granularity: "hours", - bucketMaxSpanSeconds: 60 * 60 * 24 * 30 -}); +if (!TimeseriesTest.timeseriesScalabilityImprovementsEnabled(testDB)) { + // A bucketMaxSpanSeconds may be provided, but only if they are the default for the granularity. + testValidTimeseriesOptions({ + timeField: "time", + metaField: "meta", + granularity: "seconds", + bucketMaxSpanSeconds: 60 * 60 + }); + testValidTimeseriesOptions({ + timeField: "time", + metaField: "meta", + granularity: "minutes", + bucketMaxSpanSeconds: 60 * 60 * 24 + }); + testValidTimeseriesOptions({ + timeField: "time", + metaField: "meta", + granularity: "hours", + bucketMaxSpanSeconds: 60 * 60 * 24 * 30 + }); +} else { + // Granularity may be provided with bucketMaxSpanSeconds and bucketRoundingSeconds, + // but only if they are the default (seconds)). + testValidTimeseriesOptions({ + timeField: "time", + metaField: "meta", + granularity: "seconds", + bucketMaxSpanSeconds: 60 * 60, + bucketRoundingSeconds: 60 + }); + testValidTimeseriesOptions({ + timeField: "time", + metaField: "meta", + granularity: "minutes", + bucketMaxSpanSeconds: 60 * 60 * 24, + bucketRoundingSeconds: 60 * 60, + }, + bucketGranularityError); + testValidTimeseriesOptions({ + timeField: "time", + metaField: "meta", + granularity: "hours", + bucketMaxSpanSeconds: 60 * 60 * 24 * 30, + bucketRoundingSeconds: 60 * 60 * 24, + }, + bucketGranularityError); +} testValidTimeseriesOptions({timeField: "time", metaField: "meta", granularity: "minutes"}); testValidTimeseriesOptions({timeField: "time", metaField: "meta", granularity: "hours"}); @@ -121,10 +158,10 @@ testInvalidTimeseriesOptions({timeField: "time", metaField: "sub.meta"}, ErrorCo testInvalidTimeseriesOptions({timeField: "time", metaField: "time"}, ErrorCodes.InvalidOptions); testInvalidTimeseriesOptions({timeField: "time", metaField: "meta", bucketMaxSpanSeconds: 10}, - 5510500); + bucketMaxSpanSecondsError); testInvalidTimeseriesOptions( {timeField: "time", metaField: "meta", granularity: 'minutes', bucketMaxSpanSeconds: 3600}, - 5510500); + bucketMaxSpanSecondsError); testCompatibleCreateOptions({expireAfterSeconds: NumberLong(100)}); testCompatibleCreateOptions({storageEngine: {}}); @@ -158,7 +195,6 @@ testTimeseriesNamespaceExists((testDB, collName) => { // Tests that schema validation is enabled on the bucket collection. { - const testDB = conn.getDB(dbName); const coll = testDB.getCollection('timeseries_' + collCount++); coll.drop(); assert.commandWorked( diff --git a/src/mongo/db/catalog/create_collection.cpp b/src/mongo/db/catalog/create_collection.cpp index b190ef52dc2..02a3a421755 100644 --- a/src/mongo/db/catalog/create_collection.cpp +++ b/src/mongo/db/catalog/create_collection.cpp @@ -262,32 +262,12 @@ Status _createTimeseries(OperationContext* opCtx, CollectionOptions options = optionsArg; - // TODO (SERVER-67598) Modify this comment as it will be out of date. - // Users may not pass a 'bucketMaxSpanSeconds' or 'bucketRoundingSeconds' other than the - // default. Instead they should rely on the default behavior from the 'granularity'. - auto granularity = options.timeseries->getGranularity(); - if (feature_flags::gTimeseriesScalabilityImprovements.isEnabled( - serverGlobalParams.featureCompatibility) && - options.timeseries->getBucketRoundingSeconds()) { - uassert( - 6759501, - "Timeseries 'bucketMaxSpanSeconds' needs to be set alongside 'bucketRoundingSeconds'", - options.timeseries->getBucketMaxSpanSeconds()); - - auto roundingSeconds = timeseries::getBucketRoundingSecondsFromGranularity(granularity); - // TODO (SERVER-67598): add checks for bucketRoundingSeconds (that it divides evenly and is - // less than bucketMaxSpanSeconds) - options.timeseries->setBucketRoundingSeconds(roundingSeconds); - } + Status timeseriesOptionsValidateAndSetStatus = + timeseries::validateAndSetBucketingParameters(options.timeseries.get()); - auto maxSpanSeconds = timeseries::getMaxSpanSecondsFromGranularity(granularity); - uassert(5510500, - fmt::format("Timeseries 'bucketMaxSpanSeconds' is not configurable to a value other " - "than the default of {} for the provided granularity", - maxSpanSeconds), - !options.timeseries->getBucketMaxSpanSeconds() || - maxSpanSeconds == options.timeseries->getBucketMaxSpanSeconds()); - options.timeseries->setBucketMaxSpanSeconds(maxSpanSeconds); + if (!timeseriesOptionsValidateAndSetStatus.isOK()) { + return timeseriesOptionsValidateAndSetStatus; + } // Set the validator option to a JSON schema enforcing constraints on bucket documents. // This validation is only structural to prevent accidental corruption by users and diff --git a/src/mongo/db/commands/write_commands.cpp b/src/mongo/db/commands/write_commands.cpp index 8eec0e57dfe..7c812bd575e 100644 --- a/src/mongo/db/commands/write_commands.cpp +++ b/src/mongo/db/commands/write_commands.cpp @@ -1018,9 +1018,41 @@ public: "Sharded time-series buckets collection is missing time-series fields", collDesc.getTimeseriesFields()); auto granularity = collDesc.getTimeseriesFields()->getGranularity(); - auto bucketSpan = timeseries::getMaxSpanSecondsFromGranularity(granularity); - timeSeriesOptions.setGranularity(granularity); - timeSeriesOptions.setBucketMaxSpanSeconds(bucketSpan); + auto bucketMaxSpanSeconds = + collDesc.getTimeseriesFields()->getBucketMaxSpanSeconds(); + + if (granularity) { + timeSeriesOptions.setGranularity(granularity.get()); + timeSeriesOptions.setBucketMaxSpanSeconds( + timeseries::getMaxSpanSecondsFromGranularity(*granularity)); + + if (feature_flags::gTimeseriesScalabilityImprovements.isEnabled( + serverGlobalParams.featureCompatibility)) { + timeSeriesOptions.setBucketRoundingSeconds( + timeseries::getBucketRoundingSecondsFromGranularity(*granularity)); + } + } else if (!bucketMaxSpanSeconds) { + timeSeriesOptions.setGranularity(BucketGranularityEnum::Seconds); + timeSeriesOptions.setBucketMaxSpanSeconds( + timeseries::getMaxSpanSecondsFromGranularity( + *timeSeriesOptions.getGranularity())); + if (feature_flags::gTimeseriesScalabilityImprovements.isEnabled( + serverGlobalParams.featureCompatibility)) { + timeSeriesOptions.setBucketRoundingSeconds( + timeseries::getBucketRoundingSecondsFromGranularity( + *timeSeriesOptions.getGranularity())); + } + } else { + invariant(feature_flags::gTimeseriesScalabilityImprovements.isEnabled( + serverGlobalParams.featureCompatibility) && + bucketMaxSpanSeconds); + timeSeriesOptions.setBucketMaxSpanSeconds(bucketMaxSpanSeconds); + + auto bucketRoundingSeconds = + collDesc.getTimeseriesFields()->getBucketRoundingSeconds(); + invariant(bucketRoundingSeconds); + timeSeriesOptions.setBucketRoundingSeconds(bucketRoundingSeconds); + } } } diff --git a/src/mongo/db/exec/bucket_unpacker.cpp b/src/mongo/db/exec/bucket_unpacker.cpp index fd3e790ba5a..cbd67cb7dda 100644 --- a/src/mongo/db/exec/bucket_unpacker.cpp +++ b/src/mongo/db/exec/bucket_unpacker.cpp @@ -694,7 +694,8 @@ std::pair<bool, BSONObj> BucketSpec::pushdownPredicate( int maxSpanSeconds = tsOptions.getBucketMaxSpanSeconds() ? *tsOptions.getBucketMaxSpanSeconds() - : timeseries::getMaxSpanSecondsFromGranularity(tsOptions.getGranularity()); + : timeseries::getMaxSpanSecondsFromGranularity( + tsOptions.getGranularity().get_value_or(BucketGranularityEnum::Seconds)); std::unique_ptr<MatchExpression> bucketMetricPredicate = metricPredicate ? createPredicatesOnBucketLevelField( diff --git a/src/mongo/db/s/collmod_coordinator.cpp b/src/mongo/db/s/collmod_coordinator.cpp index 9c6d352393f..c180ba3332b 100644 --- a/src/mongo/db/s/collmod_coordinator.cpp +++ b/src/mongo/db/s/collmod_coordinator.cpp @@ -178,8 +178,9 @@ ExecutorFuture<void> CollModCoordinator::_runImpl( "Invalid transition for timeseries.granularity. Can only transition " "from 'seconds' to 'minutes' or 'minutes' to 'hours'.", timeseries::isValidTimeseriesGranularityTransition( - _collInfo->timeSeriesOptions->getGranularity(), - *_request.getTimeseries()->getGranularity())); + _collInfo->timeSeriesOptions->getGranularity().get_value_or( + BucketGranularityEnum::Seconds), + _request.getTimeseries()->getGranularity().get())); } }) .then([this, executor = executor, anchor = shared_from_this()] { diff --git a/src/mongo/db/s/shard_server_catalog_cache_loader_test.cpp b/src/mongo/db/s/shard_server_catalog_cache_loader_test.cpp index ada6cf727be..0ae5ca1d584 100644 --- a/src/mongo/db/s/shard_server_catalog_cache_loader_test.cpp +++ b/src/mongo/db/s/shard_server_catalog_cache_loader_test.cpp @@ -445,6 +445,7 @@ TEST_F(ShardServerCatalogCacheLoaderTest, TimeseriesFieldsAreProperlyPropagatedO { TypeCollectionTimeseriesFields tsFields; + timeseriesOptions.setGranularity(BucketGranularityEnum::Seconds); tsFields.setTimeseriesOptions(timeseriesOptions); collectionType.setTimeseriesFields(tsFields); diff --git a/src/mongo/db/timeseries/timeseries.idl b/src/mongo/db/timeseries/timeseries.idl index 83e89ae580c..0d4fd2726b6 100644 --- a/src/mongo/db/timeseries/timeseries.idl +++ b/src/mongo/db/timeseries/timeseries.idl @@ -106,7 +106,7 @@ structs: granularity: description: "Describes the expected interval between subsequent measurements" type: BucketGranularity - default: Seconds + optional: true stability: stable bucketRoundingSeconds: description: "Used to determine the minimum time boundary when opening a new bucket diff --git a/src/mongo/db/timeseries/timeseries_options.cpp b/src/mongo/db/timeseries/timeseries_options.cpp index 250dbb33979..f19c781bcae 100644 --- a/src/mongo/db/timeseries/timeseries_options.cpp +++ b/src/mongo/db/timeseries/timeseries_options.cpp @@ -99,10 +99,14 @@ StatusWith<std::pair<TimeseriesOptions, bool>> applyTimeseriesOptionsModificatio TimeseriesOptions newOptions = currentOptions; bool changed = false; - if (auto granularity = mod.getGranularity()) { - BucketGranularityEnum target = *granularity; - if (target != currentOptions.getGranularity()) { - if (!isValidTimeseriesGranularityTransition(currentOptions.getGranularity(), target)) { + // TODO SERVER-67599 run tests for changing between custom granularity to a default one. + if (mod.getGranularity() && currentOptions.getGranularity()) { + auto granularity = mod.getGranularity(); + BucketGranularityEnum target = granularity.get(); + auto currentGranularity = currentOptions.getGranularity().get(); + + if (target != currentGranularity) { + if (!isValidTimeseriesGranularityTransition(currentGranularity, target)) { return Status{ErrorCodes::InvalidOptions, "Invalid transition for timeseries.granularity. Can only transition " "from 'seconds' to 'minutes' or 'minutes' to 'hours'."}; @@ -110,6 +114,12 @@ StatusWith<std::pair<TimeseriesOptions, bool>> applyTimeseriesOptionsModificatio newOptions.setGranularity(target); newOptions.setBucketMaxSpanSeconds( timeseries::getMaxSpanSecondsFromGranularity(target)); + + if (feature_flags::gTimeseriesScalabilityImprovements.isEnabled( + serverGlobalParams.featureCompatibility)) + newOptions.setBucketRoundingSeconds( + timeseries::getBucketRoundingSecondsFromGranularity(target)); + changed = true; } } @@ -132,16 +142,52 @@ BSONObj generateViewPipeline(const TimeseriesOptions& options, bool asArray) { } bool optionsAreEqual(const TimeseriesOptions& option1, const TimeseriesOptions& option2) { - const auto option1BucketSpan = option1.getBucketMaxSpanSeconds() - ? *option1.getBucketMaxSpanSeconds() - : getMaxSpanSecondsFromGranularity(option1.getGranularity()); - const auto option2BucketSpan = option2.getBucketMaxSpanSeconds() - ? *option2.getBucketMaxSpanSeconds() - : getMaxSpanSecondsFromGranularity(option2.getGranularity()); - return option1.getTimeField() == option1.getTimeField() && - option1.getMetaField() == option2.getMetaField() && - option1.getGranularity() == option2.getGranularity() && - option1BucketSpan == option2BucketSpan; + // The time field for both options must match. + if (option1.getTimeField() != option2.getTimeField()) { + return false; + } + + // The meta field for both options must match. + if (option1.getMetaField() != option2.getMetaField()) { + return false; + } + + auto const granularity1 = option1.getGranularity(); + auto const granularity2 = option2.getGranularity(); + + // We accept granularity as equal if they are the same or if one is + // BucketGranularityEnum::Seconds while the other one is boost::none + if (granularity1 != granularity2 && + ((!granularity1 && granularity2 != BucketGranularityEnum::Seconds) || + (granularity1 != BucketGranularityEnum::Seconds && !granularity2))) { + return false; + } + + const auto option1BucketSpan = + option1.getBucketMaxSpanSeconds().get_value_or(getMaxSpanSecondsFromGranularity( + granularity1.get_value_or(BucketGranularityEnum::Seconds))); + + const auto option2BucketSpan = + option2.getBucketMaxSpanSeconds().get_value_or(getMaxSpanSecondsFromGranularity( + granularity2.get_value_or(BucketGranularityEnum::Seconds))); + + if (option1BucketSpan != option2BucketSpan) { + return false; + } + + const auto option1BucketRounding = + option1.getBucketRoundingSeconds().get_value_or(getBucketRoundingSecondsFromGranularity( + granularity1.get_value_or(BucketGranularityEnum::Seconds))); + + const auto option2BucketRounding = + option2.getBucketRoundingSeconds().get_value_or(getBucketRoundingSecondsFromGranularity( + granularity2.get_value_or(BucketGranularityEnum::Seconds))); + + if (option1BucketRounding != option2BucketRounding) { + return false; + } + + return true; } int getBucketRoundingSecondsFromGranularity(BucketGranularityEnum granularity) { @@ -160,14 +206,94 @@ int getBucketRoundingSecondsFromGranularity(BucketGranularityEnum granularity) { } Date_t roundTimestampToGranularity(const Date_t& time, const TimeseriesOptions& options) { - auto roundingSeconds = feature_flags::gTimeseriesScalabilityImprovements.isEnabled( - serverGlobalParams.featureCompatibility) && - options.getBucketRoundingSeconds() - ? options.getBucketRoundingSeconds().value() - : getBucketRoundingSecondsFromGranularity(options.getGranularity()); + long long roundingSeconds = 0; + auto granularity = options.getGranularity(); + if (granularity) { + roundingSeconds = getBucketRoundingSecondsFromGranularity(granularity.get()); + } else { + roundingSeconds = options.getBucketRoundingSeconds().get_value_or( + getBucketRoundingSecondsFromGranularity(BucketGranularityEnum::Seconds)); + } + long long timeSeconds = durationCount<Seconds>(time.toDurationSinceEpoch()); long long roundedTimeSeconds = (timeSeconds - (timeSeconds % roundingSeconds)); return Date_t::fromDurationSinceEpoch(Seconds{roundedTimeSeconds}); } + +Status validateAndSetBucketingParameters(TimeseriesOptions& timeseriesOptions) { + auto roundingSeconds = timeseriesOptions.getBucketRoundingSeconds(); + auto maxSpanSeconds = timeseriesOptions.getBucketMaxSpanSeconds(); + auto granularity = timeseriesOptions.getGranularity(); + + bool allowSecondsParameters = feature_flags::gTimeseriesScalabilityImprovements.isEnabled( + serverGlobalParams.featureCompatibility); + bool maxSpanAndRoundingSecondsSpecified = maxSpanSeconds && roundingSeconds; + auto maxSpanSecondsFromGranularity = + getMaxSpanSecondsFromGranularity(granularity.get_value_or(BucketGranularityEnum::Seconds)); + auto roundingSecondsFromGranularity = getBucketRoundingSecondsFromGranularity( + granularity.get_value_or(BucketGranularityEnum::Seconds)); + + if (allowSecondsParameters) { + if (granularity) { + if (maxSpanSeconds && maxSpanSeconds != maxSpanSecondsFromGranularity) { + return Status{ + ErrorCodes::InvalidOptions, + fmt::format("Timeseries 'bucketMaxSpanSeconds' is not configurable to a value " + "other than the default of {} for the provided granularity", + maxSpanSecondsFromGranularity)}; + } + + if (roundingSeconds && roundingSeconds != roundingSecondsFromGranularity) { + return Status{ + ErrorCodes::InvalidOptions, + fmt::format("Timeseries 'bucketRoundingSeconds' is not configurable to a value " + "other than the default of {} for the provided granularity", + roundingSecondsFromGranularity)}; + } + + if (!maxSpanSeconds) + timeseriesOptions.setBucketMaxSpanSeconds(maxSpanSecondsFromGranularity); + + if (!roundingSeconds) + timeseriesOptions.setBucketRoundingSeconds(roundingSecondsFromGranularity); + + return Status::OK(); + } + + if (!maxSpanAndRoundingSecondsSpecified && (maxSpanSeconds || roundingSeconds)) { + return Status{ + ErrorCodes::InvalidOptions, + "Timeseries 'bucketMaxSpanSeconds' and 'bucketRoundingSeconds' need to be " + "set alongside each other"}; + } + + if (roundingSeconds != maxSpanSeconds) { + return Status{ErrorCodes::InvalidOptions, + "Timeseries 'bucketRoundingSeconds' needs to be equal to " + "'bucketMaxSpanSeconds'"}; + } + + if (!maxSpanSeconds) { + timeseriesOptions.setBucketMaxSpanSeconds(maxSpanSecondsFromGranularity); + timeseriesOptions.setBucketRoundingSeconds(roundingSecondsFromGranularity); + timeseriesOptions.setGranularity(BucketGranularityEnum::Seconds); + } + } else { + if (maxSpanSeconds && maxSpanSecondsFromGranularity != maxSpanSeconds) { + return Status{ + ErrorCodes::InvalidOptions, + fmt::format("Timeseries 'bucketMaxSpanSeconds' is not configurable to a value " + "other than the default of {} for the provided granularity", + maxSpanSecondsFromGranularity)}; + } + if (!granularity) + timeseriesOptions.setGranularity(BucketGranularityEnum::Seconds); + + if (!maxSpanSeconds) + timeseriesOptions.setBucketMaxSpanSeconds(maxSpanSecondsFromGranularity); + } + + return Status::OK(); +} } // namespace timeseries } // namespace mongo diff --git a/src/mongo/db/timeseries/timeseries_options.h b/src/mongo/db/timeseries/timeseries_options.h index 6c95cb01e3c..9704704a5ad 100644 --- a/src/mongo/db/timeseries/timeseries_options.h +++ b/src/mongo/db/timeseries/timeseries_options.h @@ -65,5 +65,12 @@ int getBucketRoundingSecondsFromGranularity(BucketGranularityEnum granularity); * Rounds down timestamp to the specified granularity. */ Date_t roundTimestampToGranularity(const Date_t& time, const TimeseriesOptions& options); + +/** + * Validates the combination of bucketRoundingSeconds, bucketMaxSpanSeconds and granularity in + * TimeseriesOptions. If the parameters are not valid we return a bad status and if no parameters + * are passed through we set them to their default values. + */ +Status validateAndSetBucketingParameters(TimeseriesOptions& timeseriesOptions); } // namespace timeseries } // namespace mongo diff --git a/src/mongo/s/catalog_cache_test.cpp b/src/mongo/s/catalog_cache_test.cpp index 698b6fded46..d03fb84ea89 100644 --- a/src/mongo/s/catalog_cache_test.cpp +++ b/src/mongo/s/catalog_cache_test.cpp @@ -27,6 +27,7 @@ * it in the license file. */ +#include "mongo/db/timeseries/timeseries_gen.h" #include "mongo/s/catalog/type_database_gen.h" #include "mongo/s/catalog_cache.h" #include "mongo/s/catalog_cache_loader_mock.h" @@ -335,9 +336,10 @@ TEST_F(CatalogCacheTest, TimeseriesFieldsAreProperlyPropagatedOnCC) { auto chunks = makeChunks(version); auto timeseriesOptions = TimeseriesOptions("fieldName"); - // 1st refresh: we should find a bucket granularity of seconds (default) + // 1st refresh: we should find a bucket granularity of seconds { TypeCollectionTimeseriesFields tsFields; + timeseriesOptions.setGranularity(BucketGranularityEnum::Seconds); tsFields.setTimeseriesOptions(timeseriesOptions); coll.setTimeseriesFields(tsFields); diff --git a/src/mongo/s/query/cluster_aggregate.cpp b/src/mongo/s/query/cluster_aggregate.cpp index 807745b6caa..aad967a2a98 100644 --- a/src/mongo/s/query/cluster_aggregate.cpp +++ b/src/mongo/s/query/cluster_aggregate.cpp @@ -55,6 +55,7 @@ #include "mongo/db/query/explain_common.h" #include "mongo/db/query/find_common.h" #include "mongo/db/query/fle/server_rewrite.h" +#include "mongo/db/timeseries/timeseries_gen.h" #include "mongo/db/timeseries/timeseries_options.h" #include "mongo/db/views/resolved_view.h" #include "mongo/db/views/view.h" @@ -234,9 +235,19 @@ void performValidationChecks(const OperationContext* opCtx, * Rebuilds the pipeline and uses a different granularity value for the 'bucketMaxSpanSeconds' field * in the $_internalUnpackBucket stage. */ -std::vector<BSONObj> rebuildPipelineWithTimeSeriesGranularity(const std::vector<BSONObj>& pipeline, - BucketGranularityEnum granularity) { - const auto bucketSpan = timeseries::getMaxSpanSecondsFromGranularity(granularity); +std::vector<BSONObj> rebuildPipelineWithTimeSeriesGranularity( + const std::vector<BSONObj>& pipeline, + boost::optional<BucketGranularityEnum> granularity, + boost::optional<int32_t> maxSpanSeconds) { + int32_t bucketSpan = 0; + + if (maxSpanSeconds) { + bucketSpan = *maxSpanSeconds; + } else { + bucketSpan = timeseries::getMaxSpanSecondsFromGranularity( + granularity.get_value_or(BucketGranularityEnum::Seconds)); + } + std::vector<BSONObj> newPipeline; for (auto& stage : pipeline) { if (stage.firstElementFieldNameStringData() == @@ -579,7 +590,9 @@ Status ClusterAggregate::retryOnViewError(OperationContext* opCtx, const auto& cm = executionNsRoutingInfoStatus.getValue(); if (cm.isSharded() && cm.getTimeseriesFields()) { const auto patchedPipeline = rebuildPipelineWithTimeSeriesGranularity( - resolvedAggRequest.getPipeline(), cm.getTimeseriesFields()->getGranularity()); + resolvedAggRequest.getPipeline(), + cm.getTimeseriesFields()->getGranularity(), + cm.getTimeseriesFields()->getBucketMaxSpanSeconds()); resolvedAggRequest.setPipeline(patchedPipeline); snapshotCm = cm; } |