diff --git a/pkg/domain/ru_stats.go b/pkg/domain/ru_stats.go index e04062745e05a..42dd328f922be 100644 --- a/pkg/domain/ru_stats.go +++ b/pkg/domain/ru_stats.go @@ -108,12 +108,29 @@ func (do *Domain) requestUnitsWriterLoop() { } // GetLastExpectedTime return the last written ru time. +// NOTE: +// - due to DST(daylight saving time), the actual duration for a specific +// time may be shorter or longer than the interval when DST happens. +// - All the tidb-server should be deployed in the same timezone to ensure +// the duration is calculated correctly. +// - The interval must not be longer than 24h. func GetLastExpectedTime(now time.Time, interval time.Duration) time.Time { - nowTs := now.Unix() - intervalSecs := int64(interval / time.Second) - tzOffset := time.Date(1971, 1, 1, 0, 0, 0, 0, time.Local).Unix() - targetTs := nowTs - (nowTs+intervalSecs-tzOffset)%intervalSecs - return time.Unix(targetTs, 0) + return GetLastExpectedTimeTZ(now, interval, time.Local) +} + +// GetLastExpectedTimeTZ return the last written ru time under specifical timezone. +// make it public only for test. +func GetLastExpectedTimeTZ(now time.Time, interval time.Duration, tz *time.Location) time.Time { + if tz == nil { + tz = time.Local + } + year, month, day := now.Date() + start := time.Date(year, month, day, 0, 0, 0, 0, tz) + // cast to int64 to bypass the durationcheck lint. + count := int64(now.Sub(start) / interval) + targetDur := time.Duration(count) * interval + // use UTC timezone to calculate target time so it can be compatible with DST. + return start.In(time.UTC).Add(targetDur).In(tz) } // DoWriteRUStatistics write ru historical data into mysql.request_unit_by_group. diff --git a/pkg/domain/ru_stats_test.go b/pkg/domain/ru_stats_test.go index 9b16dee93f931..59273ba28b248 100644 --- a/pkg/domain/ru_stats_test.go +++ b/pkg/domain/ru_stats_test.go @@ -29,6 +29,18 @@ import ( ) func TestWriteRUStatistics(t *testing.T) { + tz, _ := time.LoadLocation("Asia/Shanghai") + testWriteRUStatisticsTz(t, tz) + + // test with DST timezone. + tz, _ = time.LoadLocation("Australia/Lord_Howe") + testWriteRUStatisticsTz(t, tz) + + testWriteRUStatisticsTz(t, time.Local) + testWriteRUStatisticsTz(t, time.UTC) +} + +func testWriteRUStatisticsTz(t *testing.T, tz *time.Location) { store, dom := testkit.CreateMockStoreAndDomain(t) tk := newTestKit(t, store) @@ -70,22 +82,22 @@ func TestWriteRUStatistics(t *testing.T) { tk.MustQuery("SELECT count(*) from mysql.request_unit_by_group").Check(testkit.Rows("0")) - testRUWriter.StartTime = time.Date(2023, 12, 26, 0, 0, 1, 0, time.Local) + testRUWriter.StartTime = time.Date(2023, 12, 26, 0, 0, 1, 0, tz) require.NoError(t, testRUWriter.DoWriteRUStatistics(context.Background())) tk.MustQuery("SELECT resource_group, total_ru from mysql.request_unit_by_group").Check(testkit.Rows("default 350", "test 150")) // after 1 day, only 1 group has delta ru. testRMClient.groups[1].RUStats.RRU = 500 - testRUWriter.StartTime = time.Date(2023, 12, 27, 0, 0, 1, 0, time.Local) + testRUWriter.StartTime = time.Date(2023, 12, 27, 0, 0, 1, 0, tz) require.NoError(t, testRUWriter.DoWriteRUStatistics(context.Background())) tk.MustQuery("SELECT resource_group, total_ru from mysql.request_unit_by_group where end_time = '2023-12-27'").Check(testkit.Rows("test 400")) // test after 1 day with 0 delta ru, no data inserted. - testRUWriter.StartTime = time.Date(2023, 12, 28, 0, 0, 1, 0, time.Local) + testRUWriter.StartTime = time.Date(2023, 12, 28, 0, 0, 1, 0, tz) require.NoError(t, testRUWriter.DoWriteRUStatistics(context.Background())) tk.MustQuery("SELECT count(*) from mysql.request_unit_by_group where end_time = '2023-12-28'").Check(testkit.Rows("0")) - testRUWriter.StartTime = time.Date(2023, 12, 29, 0, 0, 0, 0, time.Local) + testRUWriter.StartTime = time.Date(2023, 12, 29, 0, 0, 0, 0, tz) testRMClient.groups[0].RUStats.WRU = 200 require.NoError(t, testRUWriter.DoWriteRUStatistics(context.Background())) tk.MustQuery("SELECT resource_group, total_ru from mysql.request_unit_by_group where end_time = '2023-12-29'").Check(testkit.Rows("default 50")) @@ -94,12 +106,12 @@ func TestWriteRUStatistics(t *testing.T) { // This is to test after restart, no unexpected data are inserted. testRMClient.groups[0].RUStats.RRU = 1000 testRMClient.groups[1].RUStats.WRU = 2000 - testRUWriter.StartTime = time.Date(2023, 12, 29, 1, 0, 0, 0, time.Local) + testRUWriter.StartTime = time.Date(2023, 12, 29, 1, 0, 0, 0, tz) require.NoError(t, testRUWriter.DoWriteRUStatistics(context.Background())) tk.MustQuery("SELECT resource_group, total_ru from mysql.request_unit_by_group where end_time = '2023-12-29'").Check(testkit.Rows("default 50")) // after 61 days, old record should be GCed. - testRUWriter.StartTime = time.Date(2023, 12, 26, 0, 0, 0, 0, time.Local).Add(92 * 24 * time.Hour) + testRUWriter.StartTime = time.Date(2023, 12, 26, 0, 0, 0, 0, tz).Add(92 * 24 * time.Hour) tk.MustQuery("SELECT count(*) from mysql.request_unit_by_group where end_time = '2023-12-26'").Check(testkit.Rows("2")) require.NoError(t, testRUWriter.GCOutdatedRecords(testRUWriter.StartTime)) tk.MustQuery("SELECT count(*) from mysql.request_unit_by_group where end_time = '2023-12-26'").Check(testkit.Rows("0")) @@ -130,20 +142,30 @@ func (is *testInfoschema) SchemaMetaVersion() int64 { } func TestGetLastExpectedTime(t *testing.T) { + tz, _ := time.LoadLocation("Asia/Shanghai") + testGetLastExpectedTimeTz(t, tz) + + // test with DST affected timezone. + tz, _ = time.LoadLocation("Australia/Lord_Howe") + testGetLastExpectedTimeTz(t, tz) + testGetLastExpectedTimeTz(t, time.Local) +} + +func testGetLastExpectedTimeTz(t *testing.T, tz *time.Location) { // 2023-12-28 10:46:23.000 - now := time.Date(2023, 12, 28, 10, 46, 23, 0, time.Local) + now := time.Date(2023, 12, 28, 10, 46, 23, 0, tz) newTime := func(hour, minute int) time.Time { - return time.Date(2023, 12, 28, hour, minute, 0, 0, time.Local) + return time.Date(2023, 12, 28, hour, minute, 0, 0, tz) } - require.Equal(t, domain.GetLastExpectedTime(now, 5*time.Minute), newTime(10, 45)) - require.Equal(t, domain.GetLastExpectedTime(time.Date(2023, 12, 28, 10, 45, 0, 0, time.Local), 5*time.Minute), newTime(10, 45)) - require.Equal(t, domain.GetLastExpectedTime(now, 10*time.Minute), newTime(10, 40)) - require.Equal(t, domain.GetLastExpectedTime(now, 30*time.Minute), newTime(10, 30)) - require.Equal(t, domain.GetLastExpectedTime(now, time.Hour), newTime(10, 0)) - require.Equal(t, domain.GetLastExpectedTime(now, 3*time.Hour), newTime(9, 0)) - require.Equal(t, domain.GetLastExpectedTime(now, 4*time.Hour), newTime(8, 0)) - require.Equal(t, domain.GetLastExpectedTime(now, 12*time.Hour), newTime(0, 0)) - require.Equal(t, domain.GetLastExpectedTime(now, 24*time.Hour), newTime(0, 0)) - require.Equal(t, domain.GetLastExpectedTime(time.Date(2023, 12, 28, 0, 0, 0, 0, time.Local), 24*time.Hour), newTime(0, 0)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 5*time.Minute, tz), newTime(10, 45)) + require.Equal(t, domain.GetLastExpectedTimeTZ(time.Date(2023, 12, 28, 10, 45, 0, 0, tz), 5*time.Minute, tz), newTime(10, 45)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 10*time.Minute, tz), newTime(10, 40)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 30*time.Minute, tz), newTime(10, 30)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, time.Hour, tz), newTime(10, 0)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 3*time.Hour, tz), newTime(9, 0)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 4*time.Hour, tz), newTime(8, 0)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 12*time.Hour, tz), newTime(0, 0)) + require.Equal(t, domain.GetLastExpectedTimeTZ(now, 24*time.Hour, tz), newTime(0, 0)) + require.Equal(t, domain.GetLastExpectedTimeTZ(time.Date(2023, 12, 28, 0, 0, 0, 0, tz), 24*time.Hour, tz), newTime(0, 0)) }