Skip to content

Commit

Permalink
SERVER-67816 Fix time rounding for timeseries control block of dates …
Browse files Browse the repository at this point in the history
…prior to 1970 (#28517)

GitOrigin-RevId: 3d3923d5bbf86308a3fc894bfdf93f4cff849f1e
  • Loading branch information
henrikedin authored and MongoDB Bot committed Nov 26, 2024
1 parent 4682e5f commit 92f5575
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/mongo/db/timeseries/timeseries_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,16 @@ Date_t roundTimestampToGranularity(const Date_t& time, const TimeseriesOptions&

long long timeSeconds = durationCount<Seconds>(time.toDurationSinceEpoch());
long long roundedTimeSeconds = (timeSeconds - (timeSeconds % roundingSeconds));
// Make sure we always round down and not towards epoch, even for dates prior to 1970 with a
// negative duration since epoch.
if (roundedTimeSeconds > timeSeconds) {
roundedTimeSeconds -= roundingSeconds;
// It is not possible that we underflowed when performing the subtraction above. Because
// we've converted the dates in milliseconds to seconds there is tons of integer space left
// for the subtraction. We'd need to have a gigantic amount of rounding seconds to be able
// to overflow here. Therefore we invariant over uasserting.
invariant(roundedTimeSeconds <= timeSeconds);
}
return Date_t::fromDurationSinceEpoch(Seconds{roundedTimeSeconds});
}

Expand Down
140 changes: 140 additions & 0 deletions src/mongo/db/timeseries/timeseries_options_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@

#include "mongo/platform/basic.h"

#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/posix_time/time_parsers.hpp>
#include <boost/move/utility_core.hpp>

#include "mongo/base/string_data.h"
#include "mongo/db/timeseries/timeseries_options.h"
#include "mongo/unittest/unittest.h"
#include "mongo/util/time_support.h"
Expand All @@ -42,6 +47,21 @@ auto createTimeseriesOptionsWithGranularity(BucketGranularityEnum granularity) {
return options;
}

int roundingSecondsFromGranularity(BucketGranularityEnum granularity) {
switch (granularity) {
case BucketGranularityEnum::Seconds:
// Round down to nearest minute.
return 60;
case BucketGranularityEnum::Minutes:
// Round down to nearest hour.
return 60 * 60;
case BucketGranularityEnum::Hours:
// Round down to nearest day.
return 60 * 60 * 24;
}
MONGO_UNREACHABLE;
}

TEST(TimeseriesOptionsTest, RoundTimestampToGranularity) {
TimeseriesOptions optionsSeconds =
createTimeseriesOptionsWithGranularity(BucketGranularityEnum::Seconds);
Expand Down Expand Up @@ -72,4 +92,124 @@ TEST(TimeseriesOptionsTest, RoundTimestampToGranularity) {
}
}

TEST(TimeseriesOptionsTest, RoundTimestampBySeconds) {
std::vector<std::tuple<BucketGranularityEnum, std::string, std::string>> testCases{
{BucketGranularityEnum::Seconds, "2024-08-08T00:00:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T00:00:00.001Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T00:00:15.555Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T00:00:30.555Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T00:00:45.555Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T00:00:59.999Z", "2024-08-08T00:00:00.000Z"},

{BucketGranularityEnum::Seconds, "2024-08-08T05:04:00.000Z", "2024-08-08T05:04:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T05:04:00.001Z", "2024-08-08T05:04:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T05:04:15.555Z", "2024-08-08T05:04:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T05:04:30.555Z", "2024-08-08T05:04:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T05:04:45.555Z", "2024-08-08T05:04:00.000Z"},
{BucketGranularityEnum::Seconds, "2024-08-08T05:04:59.999Z", "2024-08-08T05:04:00.000Z"},

{BucketGranularityEnum::Minutes, "2024-08-08T00:00:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Minutes, "2024-08-08T00:00:00.001Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Minutes, "2024-08-08T00:15:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Minutes, "2024-08-08T00:30:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Minutes, "2024-08-08T00:45:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Minutes, "2024-08-08T00:59:59.999Z", "2024-08-08T00:00:00.000Z"},

{BucketGranularityEnum::Hours, "2024-08-08T00:00:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Hours, "2024-08-08T00:00:00.001Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Hours, "2024-08-08T06:00:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Hours, "2024-08-08T12:00:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Hours, "2024-08-08T18:00:00.000Z", "2024-08-08T00:00:00.000Z"},
{BucketGranularityEnum::Hours, "2024-08-08T23:59:59.999Z", "2024-08-08T00:00:00.000Z"},
};

for (const auto& [roundingGranularity, input, expectedOutput] : testCases) {
auto inputDate = dateFromISOString(input);
ASSERT_OK(inputDate);
auto roundedDate = timeseries::roundTimestampToGranularity(
inputDate.getValue(), createTimeseriesOptionsWithGranularity(roundingGranularity));
ASSERT_EQ(dateToISOStringUTC(roundedDate), expectedOutput);
}
}

TEST(TimeseriesOptionsTest, ExtendedRangeRoundTimestamp) {
std::vector<std::tuple<BucketGranularityEnum, std::string, std::string>> testCases{
{BucketGranularityEnum::Seconds, "1901-01-01T00:00:12.345", "1901-01-01T00:00:00"},
{BucketGranularityEnum::Seconds, "1901-01-01T00:04:12.345", "1901-01-01T00:04:00"},
{BucketGranularityEnum::Seconds, "1901-01-01T02:04:12.345", "1901-01-01T02:04:00"},

{BucketGranularityEnum::Seconds, "1969-01-01T00:00:12.345", "1969-01-01T00:00:00"},
{BucketGranularityEnum::Seconds, "1969-01-01T00:04:12.345", "1969-01-01T00:04:00"},
{BucketGranularityEnum::Seconds, "1969-01-01T02:04:12.345", "1969-01-01T02:04:00"},

{BucketGranularityEnum::Seconds, "2040-01-01T00:00:12.345", "2040-01-01T00:00:00"},
{BucketGranularityEnum::Seconds, "2040-01-01T00:04:12.345", "2040-01-01T00:04:00"},
{BucketGranularityEnum::Seconds, "2040-01-01T02:04:12.345", "2040-01-01T02:04:00"},

{BucketGranularityEnum::Seconds, "2108-01-01T00:00:12.345", "2108-01-01T00:00:00"},
{BucketGranularityEnum::Seconds, "2108-01-01T00:04:12.345", "2108-01-01T00:04:00"},
{BucketGranularityEnum::Seconds, "2108-01-01T02:04:12.345", "2108-01-01T02:04:00"},

{BucketGranularityEnum::Minutes, "1901-01-01T00:00:12.345", "1901-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "1901-01-01T00:04:12.345", "1901-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "1901-01-01T02:04:12.345", "1901-01-01T02:00:00"},

{BucketGranularityEnum::Minutes, "1969-01-01T00:00:12.345", "1969-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "1969-01-01T00:04:12.345", "1969-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "1969-01-01T02:04:12.345", "1969-01-01T02:00:00"},

{BucketGranularityEnum::Minutes, "2040-01-01T00:00:12.345", "2040-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "2040-01-01T00:04:12.345", "2040-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "2040-01-01T02:04:12.345", "2040-01-01T02:00:00"},

{BucketGranularityEnum::Minutes, "2108-01-01T00:00:12.345", "2108-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "2108-01-01T00:04:12.345", "2108-01-01T00:00:00"},
{BucketGranularityEnum::Minutes, "2108-01-01T02:04:12.345", "2108-01-01T02:00:00"},

{BucketGranularityEnum::Hours, "1901-01-01T00:00:12.345", "1901-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "1901-01-01T00:04:12.345", "1901-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "1901-01-01T02:04:12.345", "1901-01-01T00:00:00"},

{BucketGranularityEnum::Hours, "1969-01-01T00:00:12.345", "1969-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "1969-01-01T00:04:12.345", "1969-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "1969-01-01T02:04:12.345", "1969-01-01T00:00:00"},

{BucketGranularityEnum::Hours, "2040-01-01T00:00:12.345", "2040-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "2040-01-01T00:04:12.345", "2040-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "2040-01-01T02:04:12.345", "2040-01-01T00:00:00"},

{BucketGranularityEnum::Hours, "2108-01-01T00:00:12.345", "2108-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "2108-01-01T00:04:12.345", "2108-01-01T00:00:00"},
{BucketGranularityEnum::Hours, "2108-01-01T02:04:12.345", "2108-01-01T00:00:00"},
};

// TODO SERVER-94228: Support ISO 8601 date parsing and formatting of dates prior to 1970.
static constexpr auto epoch = boost::posix_time::ptime(boost::gregorian::date(1970, 1, 1));
auto parse = [](const std::string& input) {
auto ptime = boost::posix_time::from_iso_extended_string(input);
return Date_t::fromMillisSinceEpoch((ptime - epoch).total_milliseconds());
};
auto format = [](Date_t date) {
boost::posix_time::milliseconds ms(date.toMillisSinceEpoch());
return boost::posix_time::to_iso_extended_string(epoch + ms);
};

for (const auto& [roundingGranularity, input, expectedOutput] : testCases) {
Date_t inputDate = parse(input);
auto roundedDate = timeseries::roundTimestampToGranularity(
inputDate, createTimeseriesOptionsWithGranularity(roundingGranularity));
// We should always round down
ASSERT_LTE(roundedDate, inputDate);
// The rounding amount should be less than the rounding seconds
ASSERT_LT((inputDate - roundedDate).count(),
roundingSecondsFromGranularity(roundingGranularity) * 1000);
// Ensure that we've rounded to an even number according to our rounding seconds
ASSERT_EQ(durationCount<Seconds>(roundedDate.toDurationSinceEpoch()) %
roundingSecondsFromGranularity(roundingGranularity),
0);
// Validate the expected output
ASSERT_EQ(format(roundedDate), expectedOutput);
}
}

} // namespace mongo

0 comments on commit 92f5575

Please sign in to comment.