Skip to content

Commit

Permalink
ttl: use static expression context to caculate the ttl expire time
Browse files Browse the repository at this point in the history
  • Loading branch information
lcwangchao committed May 8, 2024
1 parent 659f32a commit 0007b78
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 161 deletions.
1 change: 1 addition & 0 deletions pkg/ddl/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ go_library(
"//pkg/table/tables",
"//pkg/tablecodec",
"//pkg/tidb-binlog/pump_client",
"//pkg/ttl/cache",
"//pkg/types",
"//pkg/types/parser_driver",
"//pkg/util",
Expand Down
24 changes: 6 additions & 18 deletions pkg/ddl/ttl.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@
package ddl

import (
"fmt"
"strings"
"time"

"github.com/pingcap/errors"
"github.com/pingcap/tidb/pkg/expression"
"github.com/pingcap/tidb/pkg/meta"
"github.com/pingcap/tidb/pkg/parser"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/format"
"github.com/pingcap/tidb/pkg/parser/model"
"github.com/pingcap/tidb/pkg/parser/mysql"
"github.com/pingcap/tidb/pkg/sessionctx"
"github.com/pingcap/tidb/pkg/sessiontxn"
"github.com/pingcap/tidb/pkg/ttl/cache"
"github.com/pingcap/tidb/pkg/types"
"github.com/pingcap/tidb/pkg/util/dbterror"
)
Expand Down Expand Up @@ -97,7 +96,7 @@ func onTTLInfoChange(d *ddlCtx, t *meta.Meta, job *model.Job) (ver int64, err er
}

func checkTTLInfoValid(ctx sessionctx.Context, schema model.CIStr, tblInfo *model.TableInfo) error {
if err := checkTTLIntervalExpr(ctx.GetExprCtx(), tblInfo.TTLInfo); err != nil {
if err := checkTTLIntervalExpr(tblInfo.TTLInfo); err != nil {
return err
}

Expand All @@ -108,20 +107,9 @@ func checkTTLInfoValid(ctx sessionctx.Context, schema model.CIStr, tblInfo *mode
return checkTTLInfoColumnType(tblInfo)
}

func checkTTLIntervalExpr(ctx expression.BuildContext, ttlInfo *model.TTLInfo) error {
// FIXME: use a better way to validate the interval expression in ttl
var nowAddIntervalExpr ast.ExprNode

unit := ast.TimeUnitType(ttlInfo.IntervalTimeUnit)
expr := fmt.Sprintf("select NOW() + INTERVAL %s %s", ttlInfo.IntervalExprStr, unit.String())
stmts, _, err := parser.New().ParseSQL(expr)
if err != nil {
// FIXME: the error information can be wrong, as it could indicate an unknown position to user.
return errors.Trace(err)
}
nowAddIntervalExpr = stmts[0].(*ast.SelectStmt).Fields.Fields[0].Expr
_, err = expression.EvalSimpleAst(ctx, nowAddIntervalExpr)
return err
func checkTTLIntervalExpr(ttlInfo *model.TTLInfo) error {
_, err := cache.EvalExpireTime(time.Now(), ttlInfo.IntervalExprStr, ast.TimeUnitType(ttlInfo.IntervalTimeUnit))
return errors.Trace(err)
}

func checkTTLInfoColumnType(tblInfo *model.TableInfo) error {
Expand Down
5 changes: 4 additions & 1 deletion pkg/ttl/cache/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ go_library(
importpath = "github.com/pingcap/tidb/pkg/ttl/cache",
visibility = ["//visibility:public"],
deps = [
"//pkg/expression",
"//pkg/expression/contextstatic",
"//pkg/infoschema",
"//pkg/kv",
"//pkg/parser/ast",
Expand Down Expand Up @@ -48,10 +50,11 @@ go_test(
],
embed = [":cache"],
flaky = True,
shard_count = 16,
shard_count = 17,
deps = [
"//pkg/infoschema",
"//pkg/kv",
"//pkg/parser/ast",
"//pkg/parser/model",
"//pkg/server",
"//pkg/session",
Expand Down
119 changes: 54 additions & 65 deletions pkg/ttl/cache/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
"time"

"github.com/pingcap/errors"
"github.com/pingcap/tidb/pkg/expression"
"github.com/pingcap/tidb/pkg/expression/contextstatic"
"github.com/pingcap/tidb/pkg/kv"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/model"
Expand Down Expand Up @@ -193,95 +195,82 @@ func SetMockExpireTime(ctx context.Context, tm time.Time) context.Context {
}

// EvalExpireTime returns the expired time.
// It uses the global timezone to compute the expired time.
// Then we'll reset the returned expired time to the same timezone as the input `now`.
func (t *PhysicalTable) EvalExpireTime(ctx context.Context, se session.Session,
now time.Time) (expire time.Time, err error) {
if intest.InTest {
if tm, ok := ctx.Value(mockExpireTimeKey).(time.Time); ok {
return tm, err
}
}

expireExpr := t.TTLInfo.IntervalExprStr
unit := ast.TimeUnitType(t.TTLInfo.IntervalTimeUnit)

// Use the global time zone to compute expire time.
// Different timezones may have different results event with the same "now" time and TTL expression.
// Consider a TTL setting with the expiration `INTERVAL 1 MONTH`.
// If the current timezone is `Asia/Shanghai` and now is `2021-03-01 00:00:00 +0800`
// the expired time should be `2021-02-01 00:00:00 +0800`, corresponding to UTC time `2021-01-31 16:00:00 UTC`.
// But if we use the `UTC` time zone, the current time is `2021-02-28 16:00:00 UTC`,
// and the expired time should be `2021-01-28 16:00:00 UTC` that is not the same the previous one.
globalTz, err := se.GlobalTimeZone(ctx)
if err != nil {
return time.Time{}, err
}

var rows []chunk.Row

// We should set the session time zone to UTC because the next SQLs should be executed in the UTC timezone.
// The session time zone should be reverted to the original one after the SQLs are executed.
rows, err = se.ExecuteSQL(ctx, "SELECT @@time_zone")
if err != nil {
return
}

originalTZ := rows[0].GetString(0)
if _, err = se.ExecuteSQL(ctx, "SET @@time_zone='UTC'"); err != nil {
return
}

defer func() {
_, restoreErr := se.ExecuteSQL(ctx, "SET @@time_zone=%?", originalTZ)
if err == nil {
err = restoreErr
}
}()

func EvalExpireTime(now time.Time, interval string, unit ast.TimeUnitType) (time.Time, error) {
// Firstly, we should use the UTC time zone to compute the expired time to avoid time shift caused by DST.
// The start time should be a time with the same datetime string as `now` but it is in the UTC timezone.
// For example, if global timezone is `Asia/Shanghai` with a string format `2020-01-01 08:00:00 +0800`.
// The startTime should be in timezone `UTC` and have a string format `2020-01-01 08:00:00 +0000` which is not the
// same as the original one (`2020-01-01 00:00:00 +0000` in UTC actually).
nowInGlobalTZ := now.In(globalTz)
startTime := time.Date(
nowInGlobalTZ.Year(), nowInGlobalTZ.Month(), nowInGlobalTZ.Day(),
nowInGlobalTZ.Hour(), nowInGlobalTZ.Minute(), nowInGlobalTZ.Second(),
nowInGlobalTZ.Nanosecond(), time.UTC,
start := time.Date(
now.Year(), now.Month(), now.Day(),
now.Hour(), now.Minute(), now.Second(),
now.Nanosecond(), time.UTC,
)

rows, err = se.ExecuteSQL(
ctx,
// FROM_UNIXTIME does not support negative value, so we use `FROM_UNIXTIME(0) + INTERVAL <current_ts>`
// to present current time
fmt.Sprintf("SELECT FROM_UNIXTIME(0) + INTERVAL %d MICROSECOND - INTERVAL %s %s",
startTime.UnixMicro(),
expireExpr,
unit.String(),
exprCtx := contextstatic.NewStaticExprContext()
// we need to set the location to UTC to make sure the time is in the same timezone as the start time.
intest.Assert(exprCtx.GetEvalCtx().Location() == time.UTC)
expr, err := expression.ParseSimpleExpr(
exprCtx,
fmt.Sprintf("FROM_UNIXTIME(0) + INTERVAL %d MICROSECOND - INTERVAL %s %s",
start.UnixMicro(), interval, unit.String(),
),
)
if err != nil {
return time.Time{}, err
}

tm, _, err := expr.EvalTime(exprCtx.GetEvalCtx(), chunk.Row{})
if err != nil {
return
return time.Time{}, err
}

tm, err := rows[0].GetTime(0).GoTime(time.UTC)
end, err := tm.GoTime(time.UTC)
if err != nil {
return
return time.Time{}, err
}

// Then we should add the duration between the time get from the previous SQL and the start time to the now time.
expiredTime := nowInGlobalTZ.
In(now.Location()).
Add(tm.Sub(startTime)).
expiredTime := now.
Add(end.Sub(start)).
// Truncate to second to make sure the precision is always the same with the one stored in a table to avoid some
// comparing problems in testing.
Truncate(time.Second)

return expiredTime, nil
}

// EvalExpireTime returns the expired time for the current time.
// It uses the global timezone in session to evaluation the context
// and the return time is in the same timezone of now argument.
func (t *PhysicalTable) EvalExpireTime(ctx context.Context, se session.Session,
now time.Time) (time.Time, error) {
if intest.InTest {
if tm, ok := ctx.Value(mockExpireTimeKey).(time.Time); ok {
return tm, nil
}
}

// Use the global time zone to compute expire time.
// Different timezones may have different results event with the same "now" time and TTL expression.
// Consider a TTL setting with the expiration `INTERVAL 1 MONTH`.
// If the current timezone is `Asia/Shanghai` and now is `2021-03-01 00:00:00 +0800`
// the expired time should be `2021-02-01 00:00:00 +0800`, corresponding to UTC time `2021-01-31 16:00:00 UTC`.
// But if we use the `UTC` time zone, the current time is `2021-02-28 16:00:00 UTC`,
// and the expired time should be `2021-01-28 16:00:00 UTC` that is not the same the previous one.
globalTz, err := se.GlobalTimeZone(ctx)
if err != nil {
return time.Time{}, err
}

expire, err := EvalExpireTime(now.In(globalTz), t.TTLInfo.IntervalExprStr, ast.TimeUnitType(t.TTLInfo.IntervalTimeUnit))
if err != nil {
return time.Time{}, err
}

return expire.In(now.Location()), nil
}

// SplitScanRanges split ranges for TTL scan
func (t *PhysicalTable) SplitScanRanges(ctx context.Context, store kv.Storage, splitCnt int) ([]ScanRange, error) {
if len(t.KeyColumns) < 1 || splitCnt <= 1 {
Expand Down

0 comments on commit 0007b78

Please sign in to comment.