Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5c6b5e2
feat(timezone): add taosGetTZOffsetSeconds function to retrieve UTC o…
Tony2h Apr 3, 2026
f287211
refactor(time): standardize checkTime initialization to Unix epoch in…
Tony2h Apr 3, 2026
9ba4e39
feat(timezone): update checkTime initialization to use local timezone…
Tony2h Apr 7, 2026
c99c25a
test(timezone): add DST alignment test for taosTimeTruncate function
Tony2h Apr 8, 2026
99ecfaf
fix(timezone): adjust truncation logic in truncateToLocalMidnight for…
Tony2h Apr 8, 2026
92e389c
fix(timezone): remove checkTime field and update timezone offset retr…
Tony2h Apr 8, 2026
ce45e37
fix(timezone): compile error in mndGetCurrentTimezoneCheckTime
Tony2h Apr 9, 2026
f0f0dbf
fix(timezone): update comment for taosTimeTruncate to clarify histori…
Tony2h Apr 9, 2026
150e786
fix(timezone): update interval test data for accurate timezone handling
Tony2h Apr 9, 2026
2ebc6a5
fix(timezone): add error handling for local timezone offset retrieval…
Tony2h Apr 10, 2026
e8fef71
fix(timezone): remove unused taosGetTZOffsetSeconds function and improve
Tony2h Apr 10, 2026
27e4909
chore(ci): trigger pipeline
Tony2h Apr 13, 2026
ab038d4
fix(timezone): compile on mac
Tony2h Apr 13, 2026
ed8b305
fix(timezone): improve timezone offset handling and caching to avoid …
Tony2h Apr 14, 2026
cfb77ab
fix(timezone): improve error handling in timezone offset retrieval
Tony2h Apr 14, 2026
9b4ab41
fix(timezone): simplify timezone offset retrieval logic in getTZOffse…
Tony2h Apr 14, 2026
57154ff
fix(timezone): enhance timezone offset handling during DST transition…
Tony2h Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion include/common/tmsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -1973,7 +1973,14 @@ void tFreeSRetrieveFuncRsp(SRetrieveFuncRsp* pRsp);

typedef struct {
int32_t statusInterval;
int64_t checkTime; // 1970-01-01 00:00:00.000
/*
Local timezone UTC offset in seconds (east-positive, e.g. +28800 for
Asia/Shanghai). Derived from taosGetLocalTimezoneOffset() on each
status report. Paired with the timezone string in
mndCheckClusterCfgPara: a mismatch is reported only when both the
timezone string AND this offset differ.
*/
int64_t checkTime;
char timezone[TD_TIMEZONE_LEN]; // tsTimezone
Comment thread
dapan1121 marked this conversation as resolved.
char locale[TD_LOCALE_LEN]; // tsLocale
char charset[TD_LOCALE_LEN]; // tsCharset
Expand Down
45 changes: 29 additions & 16 deletions source/common/src/ttime.c
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,33 @@ int64_t taosTimeGetIntervalEnd(int64_t intervalStart, const SInterval* pInterval
1;
}

/*
getTZOffsetAtTicks - return the east-positive UTC offset (in ticks) that is
in effect at the given timestamp.

Unlike taosGetTZOffsetSeconds() which queries the offset for "now", this
function converts `ticks` to local time via taosLocalTime() and then
derives the offset as (taosTimeGm(local) - t_sec), so it correctly
resolves DST for the *target* instant.
Comment thread
dapan1121 marked this conversation as resolved.

On conversion failure 0 is returned (UTC fallback).
*/
static int64_t getTZOffsetAtTicks(int64_t ticks, int32_t precision, timezone_t tz) {
int64_t factor = TSDB_TICK_PER_SECOND(precision);
int64_t t_sec_ticks = ticks / factor;
if (ticks < 0 && ticks % factor != 0) {
t_sec_ticks -= 1;
}
time_t t_sec = (time_t)t_sec_ticks;
struct tm tm_local;
if (taosLocalTime(&t_sec, &tm_local, NULL, 0, tz) == NULL) {
uWarn("%s failed to convert ticks:%" PRId64 " to local time, code:%d",
__FUNCTION__, ticks, ERRNO);
return 0;
}
return (int64_t)(taosTimeGm(&tm_local) - t_sec) * factor;
}

int64_t taosTimeTruncate(int64_t ts, const SInterval* pInterval) {
if (pInterval->sliding == 0) {
return ts;
Expand Down Expand Up @@ -917,10 +944,7 @@ int64_t taosTimeTruncate(int64_t ts, const SInterval* pInterval) {
if (IS_CALENDAR_TIME_DURATION(pInterval->intervalUnit)) {
int64_t news = (ts / pInterval->sliding) * pInterval->sliding;
if (pInterval->slidingUnit == 'd' || pInterval->slidingUnit == 'w') {
#if defined(WINDOWS) && _MSC_VER >= 1900
int64_t timezone = _timezone;
#endif
news += (int64_t)(timezone * TSDB_TICK_PER_SECOND(precision));
news -= getTZOffsetAtTicks(news, precision, pInterval->timezone);
}

start = news;
Expand Down Expand Up @@ -952,18 +976,7 @@ int64_t taosTimeTruncate(int64_t ts, const SInterval* pInterval) {
start = (delta / pInterval->sliding + factor) * pInterval->sliding;

if (pInterval->intervalUnit == 'd' || pInterval->intervalUnit == 'w') {
/*
* here we revised the start time of day according to the local time zone,
* but in case of DST, the start time of one day need to be dynamically decided.
*/
// todo refactor to extract function that is available for Linux/Windows/Mac platform
#if defined(WINDOWS) && _MSC_VER >= 1900
// see
// https://docs.microsoft.com/en-us/cpp/c-runtime-library/daylight-dstbias-timezone-and-tzname?view=vs-2019
int64_t timezone = _timezone;
#endif

start += (int64_t)(timezone * TSDB_TICK_PER_SECOND(precision));
start -= getTZOffsetAtTicks(start, precision, pInterval->timezone);
}

int64_t end = 0;
Expand Down
143 changes: 143 additions & 0 deletions source/common/test/commonTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1106,4 +1106,147 @@ TEST(testCase, function_taosTimeTruncate) {
ASSERT_LE(res, 1633450000000);
}

/*
* Test: day-interval alignment under DST timezone (America/New_York).
*
* America/New_York has two UTC offsets:
* Winter (EST): UTC-5 (-18000s)
* Summer (EDT): UTC-4 (-14400s)
*
* For INTERVAL(1d) TIMEZONE('America/New_York'):
* - A winter timestamp should align to midnight EST = 05:00 UTC
* - A summer timestamp should align to midnight EDT = 04:00 UTC
*
* Historical BUG being tested: taosTimeTruncate internally calls
* taosGetTZOffsetSeconds() which returns the offset for "now" (the
* current wall-clock time), not the offset for the target timestamp.
Comment thread
dapan1121 marked this conversation as resolved.
* So if "now" is summer, a winter timestamp gets the EDT offset,
* making the day boundary 04:00 UTC instead of the correct
* 05:00 UTC — off by 1 hour.
Comment thread
dapan1121 marked this conversation as resolved.
*/
#ifndef WINDOWS
// RAII guard to restore global timezone even if test assertions fail mid-way.
struct TzRestoreGuard {
~TzRestoreGuard() { taosSetGlobalTimezone("Asia/Shanghai"); }
};

TEST(testCase, taosTimeTruncate_DST_day_interval) {
TzRestoreGuard tzGuard; // restores Asia/Shanghai on scope exit

Comment thread
dapan1121 marked this conversation as resolved.
// Setup: create a timezone object for America/New_York
timezone_t ny = tzalloc("America/New_York");
ASSERT_NE(ny, nullptr);

// Set global timezone too (for consistency)
ASSERT_EQ(taosSetGlobalTimezone("America/New_York"), TSDB_CODE_SUCCESS);

Comment thread
dapan1121 marked this conversation as resolved.
Comment thread
dapan1121 marked this conversation as resolved.
// -- Prepare timestamps (millisecond precision, TSDB_TIME_PRECISION_MILLI=0) --
//
// Winter: 2024-01-15 15:30:00 UTC = 2024-01-15 10:30:00 EST
// epoch_ms = 1705329000000
// Correct day start = 2024-01-15 00:00:00 EST = 2024-01-15 05:00:00 UTC
// = 1705294800000 ms
//
// Summer: 2024-07-15 15:30:00 UTC = 2024-07-15 11:30:00 EDT
// epoch_ms = 1721057400000
// Correct day start = 2024-07-15 00:00:00 EDT = 2024-07-15 04:00:00 UTC
// = 1721016000000 ms

const int64_t winter_ts_ms = 1705329000000LL; // 2024-01-15 15:30:00 UTC
const int64_t winter_day_start_ms = 1705294800000LL; // 2024-01-15 05:00:00 UTC (midnight EST)

const int64_t summer_ts_ms = 1721057400000LL; // 2024-07-15 15:30:00 UTC
const int64_t summer_day_start_ms = 1721016000000LL; // 2024-07-15 04:00:00 UTC (midnight EDT)

// Verify expected timestamps with taosLocalTime
{
time_t t;
struct tm tm_val;

// Verify winter day start is indeed midnight EST
t = (time_t)(winter_day_start_ms / 1000);
ASSERT_NE(taosLocalTime(&t, &tm_val, NULL, 0, ny), nullptr);
ASSERT_EQ(tm_val.tm_hour, 0) << "winter day start should be midnight local";
ASSERT_EQ(tm_val.tm_min, 0);
ASSERT_EQ(tm_val.tm_sec, 0);
ASSERT_EQ(tm_val.tm_mon + 1, 1); // January
ASSERT_EQ(tm_val.tm_mday, 15);

// Verify summer day start is indeed midnight EDT
t = (time_t)(summer_day_start_ms / 1000);
ASSERT_NE(taosLocalTime(&t, &tm_val, NULL, 0, ny), nullptr);
ASSERT_EQ(tm_val.tm_hour, 0) << "summer day start should be midnight local";
ASSERT_EQ(tm_val.tm_min, 0);
ASSERT_EQ(tm_val.tm_sec, 0);
ASSERT_EQ(tm_val.tm_mon + 1, 7); // July
ASSERT_EQ(tm_val.tm_mday, 15);
}

// Build SInterval for INTERVAL(1d)
const int64_t one_day_ms = 86400LL * 1000;
SInterval interval = {};
interval.timezone = ny;
interval.intervalUnit = 'd';
interval.slidingUnit = 'd';
interval.offsetUnit = 0;
interval.precision = TSDB_TIME_PRECISION_MILLI;
interval.interval = one_day_ms;
interval.sliding = one_day_ms;
interval.offset = 0;
interval.timeRange.skey = INT64_MIN;
interval.timeRange.ekey = INT64_MAX;

// -- Test winter timestamp --
int64_t winter_result = taosTimeTruncate(winter_ts_ms, &interval);

// Convert result to local time to show what we got
{
time_t t = (time_t)(winter_result / 1000);
struct tm tm_val;
taosLocalTime(&t, &tm_val, NULL, 0, ny);
std::cout << "Winter ts truncated to: "
<< (1900 + tm_val.tm_year) << "-"
<< (tm_val.tm_mon + 1) << "-" << tm_val.tm_mday
<< " " << tm_val.tm_hour << ":" << tm_val.tm_min << ":" << tm_val.tm_sec
<< " (epoch_ms=" << winter_result << ")" << std::endl;
std::cout << "Expected: epoch_ms=" << winter_day_start_ms << std::endl;
}
Comment thread
dapan1121 marked this conversation as resolved.

EXPECT_EQ(winter_result, winter_day_start_ms)
<< "Winter day boundary should be midnight EST (05:00 UTC). "
"If this fails, taosTimeTruncate used the current DST offset "
"instead of the winter offset.";

// -- Test summer timestamp --
int64_t summer_result = taosTimeTruncate(summer_ts_ms, &interval);

{
time_t t = (time_t)(summer_result / 1000);
struct tm tm_val;
taosLocalTime(&t, &tm_val, NULL, 0, ny);
std::cout << "Summer ts truncated to: "
<< (1900 + tm_val.tm_year) << "-"
<< (tm_val.tm_mon + 1) << "-" << tm_val.tm_mday
<< " " << tm_val.tm_hour << ":" << tm_val.tm_min << ":" << tm_val.tm_sec
<< " (epoch_ms=" << summer_result << ")" << std::endl;
std::cout << "Expected: epoch_ms=" << summer_day_start_ms << std::endl;
Comment thread
dapan1121 marked this conversation as resolved.
}
Comment thread
dapan1121 marked this conversation as resolved.

EXPECT_EQ(summer_result, summer_day_start_ms)
<< "Summer day boundary should be midnight EDT (04:00 UTC). "
"If this fails, taosTimeTruncate used the current DST offset "
"instead of the summer offset.";
Comment thread
dapan1121 marked this conversation as resolved.

// After the fix, BOTH seasons should align correctly.
bool both_correct = (winter_result == winter_day_start_ms) &&
(summer_result == summer_day_start_ms);
ASSERT_TRUE(both_correct)
<< "Both winter and summer day boundaries should be correct "
"now that taosTimeTruncate uses per-timestamp DST resolution.";

tzfree(ny);
// TzRestoreGuard destructor handles taosSetGlobalTimezone("Asia/Shanghai")
}
#endif

#pragma GCC diagnostic pop
13 changes: 7 additions & 6 deletions source/dnode/mgmt/mgmt_dnode/src/dmHandle.c
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ void dmSendStatusReq(SDnodeMgmt *pMgmt) {

req.clusterCfg.statusInterval = tsStatusInterval;
req.clusterCfg.statusIntervalMs = tsStatusIntervalMs;
req.clusterCfg.checkTime = 0;
req.clusterCfg.ttlChangeOnWrite = tsTtlChangeOnWrite;
req.clusterCfg.enableWhiteList = tsEnableWhiteList ? 1 : 0;
req.clusterCfg.encryptionKeyStat = tsEncryptionKeyStat;
Expand All @@ -206,12 +205,14 @@ void dmSendStatusReq(SDnodeMgmt *pMgmt) {
req.clusterCfg.monitorParas.tsSlowLogScope = tsSlowLogScope;
req.clusterCfg.monitorParas.tsSlowLogMaxLen = tsSlowLogMaxLen;
req.clusterCfg.monitorParas.tsSlowLogThreshold = tsSlowLogThreshold;
tstrncpy(req.clusterCfg.monitorParas.tsSlowLogExceptDb, tsSlowLogExceptDb, TSDB_DB_NAME_LEN);
char timestr[32] = "1970-01-01 00:00:00.00";
if (taosParseTime(timestr, &req.clusterCfg.checkTime, (int32_t)strlen(timestr), TSDB_TIME_PRECISION_MILLI, NULL) !=
0) {
dError("failed to parse time since %s", tstrerror(code));
req.clusterCfg.checkTime = (int64_t)taosGetLocalTimezoneOffset();
if (code != 0) {
dError("failed to get local timezone offset, since %s", tstrerror(code));
(void)taosThreadMutexUnlock(&pMgmt->pData->statusInfolock);
return;
}
Comment thread
dapan1121 marked this conversation as resolved.
Comment thread
dapan1121 marked this conversation as resolved.

tstrncpy(req.clusterCfg.monitorParas.tsSlowLogExceptDb, tsSlowLogExceptDb, TSDB_DB_NAME_LEN);
memcpy(req.clusterCfg.timezone, tsTimezoneStr, TD_TIMEZONE_LEN);
memcpy(req.clusterCfg.locale, tsLocale, TD_LOCALE_LEN);
memcpy(req.clusterCfg.charset, tsCharset, TD_LOCALE_LEN);
Expand Down
1 change: 0 additions & 1 deletion source/dnode/mnode/impl/inc/mndInt.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ typedef struct SMnode {
bool restored;
bool deploy;
char *path;
int64_t checkTime;
SyncIndex applied;
SSdb *pSdb;
SArray *pSteps;
Expand Down
Loading
Loading