summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--jstests/change_streams/timeseries.js6
-rw-r--r--jstests/core/timeseries/bucket_span_and_rounding_seconds.js243
-rw-r--r--jstests/core/timeseries/timeseries_list_collections.js92
-rw-r--r--jstests/core/timeseries/timeseries_list_collections_filter_name.js29
-rw-r--r--jstests/noPassthrough/timeseries_create.js74
-rw-r--r--src/mongo/db/catalog/create_collection.cpp30
-rw-r--r--src/mongo/db/commands/write_commands.cpp38
-rw-r--r--src/mongo/db/exec/bucket_unpacker.cpp3
-rw-r--r--src/mongo/db/s/collmod_coordinator.cpp5
-rw-r--r--src/mongo/db/s/shard_server_catalog_cache_loader_test.cpp1
-rw-r--r--src/mongo/db/timeseries/timeseries.idl2
-rw-r--r--src/mongo/db/timeseries/timeseries_options.cpp164
-rw-r--r--src/mongo/db/timeseries/timeseries_options.h7
-rw-r--r--src/mongo/s/catalog_cache_test.cpp4
-rw-r--r--src/mongo/s/query/cluster_aggregate.cpp21
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;
}