Skip to content

Commit

Permalink
domain: make ru_stats duration compatible with DST (pingcap#52408)
Browse files Browse the repository at this point in the history
  • Loading branch information
glorv authored Apr 8, 2024
1 parent 23f6237 commit 33f5d05
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 23 deletions.
27 changes: 22 additions & 5 deletions pkg/domain/ru_stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 40 additions & 18 deletions pkg/domain/ru_stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"))
Expand All @@ -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"))
Expand Down Expand Up @@ -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))
}

0 comments on commit 33f5d05

Please sign in to comment.