diff --git a/bindinfo/bind_test.go b/bindinfo/bind_test.go index f2501c6c9b6a7..1a037380ad114 100644 --- a/bindinfo/bind_test.go +++ b/bindinfo/bind_test.go @@ -523,4 +523,10 @@ func (s *testSuite) TestAddEvolveTasks(c *C) { c.Assert(len(rows), Equals, 2) c.Assert(rows[1][1], Equals, "SELECT /*+ USE_INDEX(@`sel_1` `test`.`t` )*/ * FROM `test`.`t` WHERE `a`>=4 AND `b`>=1 AND `c`=0") c.Assert(rows[1][3], Equals, "pending verify") + tk.MustExec("admin evolve bindings") + rows = tk.MustQuery("show global bindings").Rows() + c.Assert(len(rows), Equals, 2) + c.Assert(rows[1][1], Equals, "SELECT /*+ USE_INDEX(@`sel_1` `test`.`t` )*/ * FROM `test`.`t` WHERE `a`>=4 AND `b`>=1 AND `c`=0") + status := rows[1][3].(string) + c.Assert(status == "using" || status == "rejected", IsTrue) } diff --git a/bindinfo/cache.go b/bindinfo/cache.go index c25aed57e329f..73a3eddd7ef9b 100644 --- a/bindinfo/cache.go +++ b/bindinfo/cache.go @@ -33,6 +33,9 @@ const ( Invalid = "invalid" // PendingVerify means the bind info needs to be verified. PendingVerify = "pending verify" + // Rejected means that the bind has been rejected after verify process. + // We can retry it after certain time has passed. + Rejected = "rejected" ) // Binding stores the basic bind hint info. diff --git a/bindinfo/handle.go b/bindinfo/handle.go index 26d9c77a6b53f..439bf3de18a95 100644 --- a/bindinfo/handle.go +++ b/bindinfo/handle.go @@ -15,7 +15,10 @@ package bindinfo import ( "context" + "errors" "fmt" + "runtime" + "strconv" "strings" "sync" "sync/atomic" @@ -29,12 +32,14 @@ import ( "github.com/pingcap/tidb/infoschema" "github.com/pingcap/tidb/metrics" "github.com/pingcap/tidb/sessionctx" + "github.com/pingcap/tidb/sessionctx/variable" "github.com/pingcap/tidb/store/tikv/oracle" "github.com/pingcap/tidb/types" "github.com/pingcap/tidb/util/chunk" "github.com/pingcap/tidb/util/logutil" "github.com/pingcap/tidb/util/sqlexec" "github.com/pingcap/tidb/util/stmtsummary" + "github.com/pingcap/tidb/util/timeutil" "go.uber.org/zap" ) @@ -80,6 +85,13 @@ type BindHandle struct { // Lease influences the duration of loading bind info and handling invalid bind. var Lease = 3 * time.Second +const ( + // OwnerKey is the bindinfo owner path that is saved to etcd. + OwnerKey = "/tidb/bindinfo/owner" + // Prompt is the prompt for bindinfo owner manager. + Prompt = "bindinfo" +) + type bindRecordUpdate struct { bindRecord *BindRecord updateTime time.Time @@ -170,7 +182,7 @@ func (h *BindHandle) AddBindRecord(sctx sessionctx.Context, is infoschema.InfoSc if br != nil { binding := br.FindBinding(record.Bindings[0].id) if binding != nil { - // There is already a binding with status `Using` or `PendingVerify`, we could directly cancel the job. + // There is already a binding with status `Using`, `PendingVerify` or `Rejected`, we could directly cancel the job. if record.Bindings[0].Status == PendingVerify { return nil } @@ -463,7 +475,9 @@ func (c cache) setBindRecord(hash string, meta *BindRecord) { func (c cache) copy() cache { newCache := make(cache, len(c)) for k, v := range c { - newCache[k] = v + bindRecords := make([]*BindRecord, len(v)) + copy(bindRecords, v) + newCache[k] = bindRecords } return newCache } @@ -590,6 +604,168 @@ func (h *BindHandle) SaveEvolveTasksToStore() { h.pendingVerifyBindRecordMap.flushToStore() } +func getEvolveParameters(ctx sessionctx.Context) (time.Duration, time.Time, time.Time, error) { + sql := fmt.Sprintf("select variable_name, variable_value from mysql.global_variables where variable_name in ('%s', '%s', '%s')", + variable.TiDBEvolvePlanTaskMaxTime, variable.TiDBEvolvePlanTaskStartTime, variable.TiDBEvolvePlanTaskEndTime) + rows, _, err := ctx.(sqlexec.RestrictedSQLExecutor).ExecRestrictedSQL(sql) + if err != nil { + return 0, time.Time{}, time.Time{}, err + } + maxTime, startTimeStr, endTimeStr := int64(variable.DefTiDBEvolvePlanTaskMaxTime), variable.DefTiDBEvolvePlanTaskStartTime, variable.DefAutoAnalyzeEndTime + for _, row := range rows { + switch row.GetString(0) { + case variable.TiDBEvolvePlanTaskMaxTime: + maxTime, err = strconv.ParseInt(row.GetString(1), 10, 64) + if err != nil { + return 0, time.Time{}, time.Time{}, err + } + case variable.TiDBEvolvePlanTaskStartTime: + startTimeStr = row.GetString(1) + case variable.TiDBEvolvePlanTaskEndTime: + endTimeStr = row.GetString(1) + } + } + startTime, err := time.ParseInLocation(variable.FullDayTimeFormat, startTimeStr, time.UTC) + if err != nil { + return 0, time.Time{}, time.Time{}, err + + } + endTime, err := time.ParseInLocation(variable.FullDayTimeFormat, endTimeStr, time.UTC) + if err != nil { + return 0, time.Time{}, time.Time{}, err + } + return time.Duration(maxTime) * time.Second, startTime, endTime, nil +} + +const ( + // acceptFactor is the factor to decide should we accept the pending verified plan. + // A pending verified plan will be accepted if it performs at least `acceptFactor` times better than the accepted plans. + acceptFactor = 1.5 + // nextVerifyDuration is the duration that we will retry the rejected plans. + nextVerifyDuration = 7 * 24 * time.Hour +) + +func (h *BindHandle) getOnePendingVerifyJob() (string, string, Binding) { + cache := h.bindInfo.Value.Load().(cache) + for _, bindRecords := range cache { + for _, bindRecord := range bindRecords { + for _, bind := range bindRecord.Bindings { + if bind.Status == PendingVerify { + return bindRecord.OriginalSQL, bindRecord.Db, bind + } + if bind.Status != Rejected { + continue + } + updateTime, err := bind.UpdateTime.Time.GoTime(time.UTC) + // Should not happen. + if err != nil { + continue + } + // Rejected and retry it now. + if time.Since(updateTime) > nextVerifyDuration { + return bindRecord.OriginalSQL, bindRecord.Db, bind + } + } + } + } + return "", "", Binding{} +} + +func (h *BindHandle) getRunningDuration(sctx sessionctx.Context, db, sql string, maxTime time.Duration) (time.Duration, error) { + ctx := context.TODO() + if db != "" { + _, err := sctx.(sqlexec.SQLExecutor).Execute(ctx, fmt.Sprintf("use `%s`", db)) + if err != nil { + return 0, err + } + } + ctx, cancelFunc := context.WithCancel(ctx) + timer := time.NewTimer(maxTime) + resultChan := make(chan error) + startTime := time.Now() + go runSQL(ctx, sctx, sql, resultChan) + select { + case err := <-resultChan: + cancelFunc() + if err != nil { + return 0, err + } + return time.Since(startTime), nil + case <-timer.C: + cancelFunc() + } + <-resultChan + return -1, nil +} + +func runSQL(ctx context.Context, sctx sessionctx.Context, sql string, resultChan chan<- error) { + defer func() { + if r := recover(); r != nil { + buf := make([]byte, 4096) + stackSize := runtime.Stack(buf, false) + buf = buf[:stackSize] + resultChan <- errors.New(fmt.Sprintf("run sql panicked: %v", string(buf))) + } + }() + recordSets, err := sctx.(sqlexec.SQLExecutor).Execute(ctx, sql) + if err != nil { + terror.Call(recordSets[0].Close) + resultChan <- err + return + } + recordSet := recordSets[0] + chk := recordSets[0].NewChunk() + for { + err = recordSet.Next(ctx, chk) + if err != nil || chk.NumRows() == 0 { + break + } + } + terror.Call(recordSets[0].Close) + resultChan <- err + return +} + +// HandleEvolvePlanTask tries to evolve one plan task. +// It only handle one tasks once because we want each task could use the latest parameters. +func (h *BindHandle) HandleEvolvePlanTask(sctx sessionctx.Context) error { + originalSQL, db, binding := h.getOnePendingVerifyJob() + if originalSQL == "" { + return nil + } + maxTime, startTime, endTime, err := getEvolveParameters(sctx) + if err != nil { + return err + } + if maxTime == 0 || !timeutil.WithinDayTimePeriod(startTime, endTime, time.Now()) { + return nil + } + sctx.GetSessionVars().UsePlanBaselines = true + acceptedPlanTime, err := h.getRunningDuration(sctx, db, binding.BindSQL, maxTime) + // If we just return the error to the caller, this job will be retried again and again and cause endless logs, + // since it is still in the bind record. Now we just drop it and if it is actually retryable, + // we will hope for that we can capture this evolve task again. + if err != nil { + return h.DropBindRecord(sctx, sctx.GetSessionVars().TxnCtx.InfoSchema.(infoschema.InfoSchema), &BindRecord{OriginalSQL: originalSQL, Db: db, Bindings: []Binding{binding}}) + } + // If the accepted plan timeouts, it is hard to decide the timeout for verify plan. + // Currently we simply mark the verify plan as `using` if it could run successfully within maxTime. + if acceptedPlanTime > 0 { + maxTime = time.Duration(float64(acceptedPlanTime) / acceptFactor) + } + sctx.GetSessionVars().UsePlanBaselines = false + verifyPlanTime, err := h.getRunningDuration(sctx, db, binding.BindSQL, maxTime) + if err != nil { + return h.DropBindRecord(sctx, sctx.GetSessionVars().TxnCtx.InfoSchema.(infoschema.InfoSchema), &BindRecord{OriginalSQL: originalSQL, Db: db, Bindings: []Binding{binding}}) + } + if verifyPlanTime < 0 { + binding.Status = Rejected + } else { + binding.Status = Using + } + return h.AddBindRecord(sctx, sctx.GetSessionVars().TxnCtx.InfoSchema.(infoschema.InfoSchema), &BindRecord{OriginalSQL: originalSQL, Db: db, Bindings: []Binding{binding}}) +} + // Clear resets the bind handle. It is used for test. func (h *BindHandle) Clear() { h.bindInfo.Store(make(cache)) diff --git a/domain/domain.go b/domain/domain.go index 5ebe342804a8d..0373111eb1c71 100644 --- a/domain/domain.go +++ b/domain/domain.go @@ -830,16 +830,17 @@ func (do *Domain) BindHandle() *bindinfo.BindHandle { // LoadBindInfoLoop create a goroutine loads BindInfo in a loop, it should // be called only once in BootstrapSession. -func (do *Domain) LoadBindInfoLoop(ctx sessionctx.Context) error { - ctx.GetSessionVars().InRestrictedSQL = true - do.bindHandle = bindinfo.NewBindHandle(ctx) +func (do *Domain) LoadBindInfoLoop(ctxForHandle sessionctx.Context, ctxForEvolve sessionctx.Context) error { + ctxForHandle.GetSessionVars().InRestrictedSQL = true + ctxForEvolve.GetSessionVars().InRestrictedSQL = true + do.bindHandle = bindinfo.NewBindHandle(ctxForHandle) err := do.bindHandle.Update(true) if err != nil || bindinfo.Lease == 0 { return err } do.globalBindHandleWorkerLoop() - do.handleInvalidBindTaskLoop() + do.handleEvolvePlanTasksLoop(ctxForEvolve) return nil } @@ -862,6 +863,7 @@ func (do *Domain) globalBindHandleWorkerLoop() { if !variable.TiDBOptOn(variable.CapturePlanBaseline.GetVal()) { continue } + do.bindHandle.DropInvalidBindRecord() do.bindHandle.CaptureBaselines() do.bindHandle.SaveEvolveTasksToStore() } @@ -869,18 +871,24 @@ func (do *Domain) globalBindHandleWorkerLoop() { }() } -func (do *Domain) handleInvalidBindTaskLoop() { +func (do *Domain) handleEvolvePlanTasksLoop(ctx sessionctx.Context) { do.wg.Add(1) go func() { defer do.wg.Done() - defer recoverInDomain("loadBindInfoLoop-dropInvalidBindInfo", false) + defer recoverInDomain("handleEvolvePlanTasksLoop", false) + owner := do.newOwnerManager(bindinfo.Prompt, bindinfo.OwnerKey) for { select { case <-do.exit: return case <-time.After(bindinfo.Lease): } - do.bindHandle.DropInvalidBindRecord() + if owner.IsOwner() { + err := do.bindHandle.HandleEvolvePlanTask(ctx) + if err != nil { + logutil.BgLogger().Info("evolve plan failed", zap.Error(err)) + } + } } }() } @@ -928,7 +936,7 @@ func (do *Domain) UpdateTableStatsLoop(ctx sessionctx.Context) error { if do.statsLease <= 0 { return nil } - owner := do.newStatsOwner() + owner := do.newOwnerManager(handle.StatsPrompt, handle.StatsOwnerKey) do.wg.Add(1) do.SetStatsUpdating(true) go do.updateStatsWorker(ctx, owner) @@ -939,14 +947,14 @@ func (do *Domain) UpdateTableStatsLoop(ctx sessionctx.Context) error { return nil } -func (do *Domain) newStatsOwner() owner.Manager { +func (do *Domain) newOwnerManager(prompt, ownerKey string) owner.Manager { id := do.ddl.OwnerManager().ID() cancelCtx, cancelFunc := context.WithCancel(context.Background()) var statsOwner owner.Manager if do.etcdClient == nil { statsOwner = owner.NewMockManager(id, cancelFunc) } else { - statsOwner = owner.NewOwnerManager(do.etcdClient, handle.StatsPrompt, id, handle.StatsOwnerKey, cancelFunc) + statsOwner = owner.NewOwnerManager(do.etcdClient, prompt, id, ownerKey, cancelFunc) } // TODO: Need to do something when err is not nil. err := statsOwner.CampaignOwner(cancelCtx) diff --git a/executor/bind.go b/executor/bind.go index 14f968aa9249d..ef160bd868f70 100644 --- a/executor/bind.go +++ b/executor/bind.go @@ -49,6 +49,8 @@ func (e *SQLBindExec) Next(ctx context.Context, req *chunk.Chunk) error { return e.flushBindings() case plannercore.OpCaptureBindings: e.captureBindings() + case plannercore.OpEvolveBindings: + return e.evolveBindings() default: return errors.Errorf("unsupported SQL bind operation: %v", e.sqlBindOp) } @@ -104,3 +106,7 @@ func (e *SQLBindExec) flushBindings() error { func (e *SQLBindExec) captureBindings() { domain.GetDomain(e.ctx).BindHandle().CaptureBaselines() } + +func (e *SQLBindExec) evolveBindings() error { + return domain.GetDomain(e.ctx).BindHandle().HandleEvolvePlanTask(e.ctx) +} diff --git a/planner/optimize.go b/planner/optimize.go index 47961882ac6fd..9e4679d0b9f30 100644 --- a/planner/optimize.go +++ b/planner/optimize.go @@ -238,7 +238,11 @@ func handleEvolveTasks(ctx context.Context, sctx sessionctx.Context, br *bindinf if err != nil { logutil.Logger(ctx).Info("Restore SQL failed", zap.Error(err)) } - bindsql := strings.Replace(sb.String(), "SELECT", fmt.Sprintf("SELECT /*+ %s*/", planHint), 1) + bindSQL := sb.String() + selectIdx := strings.Index(bindSQL, "SELECT") + // Remove possible `explain` prefix. + bindSQL = bindSQL[selectIdx:] + bindsql := strings.Replace(bindSQL, "SELECT", fmt.Sprintf("SELECT /*+ %s*/", planHint), 1) globalHandle := domain.GetDomain(sctx).BindHandle() charset, collation := sctx.GetSessionVars().GetCharsetInfo() binding := bindinfo.Binding{BindSQL: bindsql, Status: bindinfo.PendingVerify, Charset: charset, Collation: collation} diff --git a/session/session.go b/session/session.go index fc00cd094b564..9353d2119e8b9 100644 --- a/session/session.go +++ b/session/session.go @@ -1640,7 +1640,11 @@ func BootstrapSession(store kv.Storage) (*domain.Domain, error) { if err != nil { return nil, err } - err = dom.LoadBindInfoLoop(se2) + se3, err := createSession(store) + if err != nil { + return nil, err + } + err = dom.LoadBindInfoLoop(se2, se3) if err != nil { return nil, err } diff --git a/sessionctx/variable/sysvar.go b/sessionctx/variable/sysvar.go index ebfa525772fe1..0aa73d83148ac 100644 --- a/sessionctx/variable/sysvar.go +++ b/sessionctx/variable/sysvar.go @@ -720,6 +720,9 @@ var defaultSysVars = []*SysVar{ {ScopeGlobal | ScopeSession, TiDBCapturePlanBaseline, "0"}, {ScopeGlobal | ScopeSession, TiDBUsePlanBaselines, BoolToIntStr(DefTiDBUsePlanBaselines)}, {ScopeGlobal | ScopeSession, TiDBEvolvePlanBaselines, BoolToIntStr(DefTiDBEvolvePlanBaselines)}, + {ScopeGlobal, TiDBEvolvePlanTaskMaxTime, strconv.Itoa(DefTiDBEvolvePlanTaskMaxTime)}, + {ScopeGlobal, TiDBEvolvePlanTaskStartTime, DefTiDBEvolvePlanTaskStartTime}, + {ScopeGlobal, TiDBEvolvePlanTaskEndTime, DefTiDBEvolvePlanTaskEndTime}, {ScopeGlobal | ScopeSession, TiDBIsolationReadEngines, "tikv,tiflash"}, {ScopeGlobal | ScopeSession, TiDBStoreLimit, strconv.FormatInt(atomic.LoadInt64(&config.GetGlobalConfig().TiKVClient.StoreLimit), 10)}, } diff --git a/sessionctx/variable/tidb_vars.go b/sessionctx/variable/tidb_vars.go index 909cd2590999f..3e8fec53ff939 100644 --- a/sessionctx/variable/tidb_vars.go +++ b/sessionctx/variable/tidb_vars.go @@ -155,6 +155,14 @@ const ( // TiDBAllowRemoveAutoInc indicates whether a user can drop the auto_increment column attribute or not. TiDBAllowRemoveAutoInc = "tidb_allow_remove_auto_inc" + + // TiDBEvolvePlanTaskMaxTime controls the max time of a single evolution task. + TiDBEvolvePlanTaskMaxTime = "tidb_evolve_plan_task_max_time" + + // TiDBEvolvePlanTaskStartTime is the start time of evolution task. + TiDBEvolvePlanTaskStartTime = "tidb_evolve_plan_task_start_time" + // TiDBEvolvePlanTaskEndTime is the end time of evolution task. + TiDBEvolvePlanTaskEndTime = "tidb_evolve_plan_task_end_time" ) // TiDB system variable names that both in session and global scope. @@ -420,6 +428,9 @@ const ( DefTiDBAllowRemoveAutoInc = false DefTiDBUsePlanBaselines = true DefTiDBEvolvePlanBaselines = false + DefTiDBEvolvePlanTaskMaxTime = 600 // 600s + DefTiDBEvolvePlanTaskStartTime = "00:00 +0000" + DefTiDBEvolvePlanTaskEndTime = "23:59 +0000" DefInnodbLockWaitTimeout = 50 // 50s DefTiDBStoreLimit = 0 ) diff --git a/sessionctx/variable/varsutil.go b/sessionctx/variable/varsutil.go index 0c10a5c900927..6f3df1a8ba0d2 100644 --- a/sessionctx/variable/varsutil.go +++ b/sessionctx/variable/varsutil.go @@ -523,14 +523,15 @@ func ValidateSetSystemVar(vars *SessionVars, name string, value string) (string, TIDBMemQuotaNestedLoopApply, TiDBRetryLimit, TiDBSlowLogThreshold, - TiDBQueryLogMaxLen: + TiDBQueryLogMaxLen, + TiDBEvolvePlanTaskMaxTime: _, err := strconv.ParseInt(value, 10, 64) if err != nil { return value, ErrWrongValueForVar.GenWithStackByArgs(name) } return value, nil - case TiDBAutoAnalyzeStartTime, TiDBAutoAnalyzeEndTime: - v, err := setAnalyzeTime(vars, value) + case TiDBAutoAnalyzeStartTime, TiDBAutoAnalyzeEndTime, TiDBEvolvePlanTaskStartTime, TiDBEvolvePlanTaskEndTime: + v, err := setDayTime(vars, value) if err != nil { return "", err } @@ -744,23 +745,23 @@ func GoTimeToTS(t time.Time) uint64 { } const ( - analyzeLocalTimeFormat = "15:04" - // AnalyzeFullTimeFormat is the full format of analyze start time and end time. - AnalyzeFullTimeFormat = "15:04 -0700" + localDayTimeFormat = "15:04" + // FullDayTimeFormat is the full format of analyze start time and end time. + FullDayTimeFormat = "15:04 -0700" ) -func setAnalyzeTime(s *SessionVars, val string) (string, error) { +func setDayTime(s *SessionVars, val string) (string, error) { var t time.Time var err error - if len(val) <= len(analyzeLocalTimeFormat) { - t, err = time.ParseInLocation(analyzeLocalTimeFormat, val, s.TimeZone) + if len(val) <= len(localDayTimeFormat) { + t, err = time.ParseInLocation(localDayTimeFormat, val, s.TimeZone) } else { - t, err = time.ParseInLocation(AnalyzeFullTimeFormat, val, s.TimeZone) + t, err = time.ParseInLocation(FullDayTimeFormat, val, s.TimeZone) } if err != nil { return "", err } - return t.Format(AnalyzeFullTimeFormat), nil + return t.Format(FullDayTimeFormat), nil } // serverGlobalVariable is used to handle variables that acts in server and global scope. diff --git a/statistics/handle/update.go b/statistics/handle/update.go index 73c44a5b03603..cba72bf2a4b12 100644 --- a/statistics/handle/update.go +++ b/statistics/handle/update.go @@ -41,6 +41,7 @@ import ( "github.com/pingcap/tidb/util/logutil" "github.com/pingcap/tidb/util/ranger" "github.com/pingcap/tidb/util/sqlexec" + "github.com/pingcap/tidb/util/timeutil" "go.uber.org/zap" ) @@ -611,21 +612,6 @@ func TableAnalyzed(tbl *statistics.Table) bool { return false } -// withinTimePeriod tests whether `now` is between `start` and `end`. -func withinTimePeriod(start, end, now time.Time) bool { - // Converts to UTC and only keeps the hour and minute info. - start, end, now = start.UTC(), end.UTC(), now.UTC() - start = time.Date(0, 0, 0, start.Hour(), start.Minute(), 0, 0, time.UTC) - end = time.Date(0, 0, 0, end.Hour(), end.Minute(), 0, 0, time.UTC) - now = time.Date(0, 0, 0, now.Hour(), now.Minute(), 0, 0, time.UTC) - // for cases like from 00:00 to 06:00 - if end.Sub(start) >= 0 { - return now.Sub(start) >= 0 && now.Sub(end) <= 0 - } - // for cases like from 22:00 to 06:00 - return now.Sub(end) <= 0 || now.Sub(start) >= 0 -} - // NeedAnalyzeTable checks if we need to analyze the table: // 1. If the table has never been analyzed, we need to analyze it when it has // not been modified for a while. @@ -648,7 +634,7 @@ func NeedAnalyzeTable(tbl *statistics.Table, limit time.Duration, autoAnalyzeRat return false, "" } // Tests if current time is within the time period. - return withinTimePeriod(start, end, now), fmt.Sprintf("too many modifications(%v/%v)", tbl.ModifyCount, tbl.Count) + return timeutil.WithinDayTimePeriod(start, end, now), fmt.Sprintf("too many modifications(%v/%v)", tbl.ModifyCount, tbl.Count) } const ( @@ -687,11 +673,11 @@ func parseAnalyzePeriod(start, end string) (time.Time, time.Time, error) { if end == "" { end = variable.DefAutoAnalyzeEndTime } - s, err := time.ParseInLocation(variable.AnalyzeFullTimeFormat, start, time.UTC) + s, err := time.ParseInLocation(variable.FullDayTimeFormat, start, time.UTC) if err != nil { return s, s, errors.Trace(err) } - e, err := time.ParseInLocation(variable.AnalyzeFullTimeFormat, end, time.UTC) + e, err := time.ParseInLocation(variable.FullDayTimeFormat, end, time.UTC) return s, e, err } diff --git a/statistics/handle/update_test.go b/statistics/handle/update_test.go index 5c9e123de0d5f..3ea8b93e311b0 100644 --- a/statistics/handle/update_test.go +++ b/statistics/handle/update_test.go @@ -1245,11 +1245,11 @@ func (s *testStatsSuite) TestNeedAnalyzeTable(c *C) { }, } for _, test := range tests { - start, err := time.ParseInLocation(variable.AnalyzeFullTimeFormat, test.start, time.UTC) + start, err := time.ParseInLocation(variable.FullDayTimeFormat, test.start, time.UTC) c.Assert(err, IsNil) - end, err := time.ParseInLocation(variable.AnalyzeFullTimeFormat, test.end, time.UTC) + end, err := time.ParseInLocation(variable.FullDayTimeFormat, test.end, time.UTC) c.Assert(err, IsNil) - now, err := time.ParseInLocation(variable.AnalyzeFullTimeFormat, test.now, time.UTC) + now, err := time.ParseInLocation(variable.FullDayTimeFormat, test.now, time.UTC) c.Assert(err, IsNil) needAnalyze, reason := handle.NeedAnalyzeTable(test.tbl, test.limit, test.ratio, start, end, now) c.Assert(needAnalyze, Equals, test.result) diff --git a/util/timeutil/time.go b/util/timeutil/time.go index 846f272ce9cc4..fba901f439bd7 100644 --- a/util/timeutil/time.go +++ b/util/timeutil/time.go @@ -169,3 +169,18 @@ func Zone(loc *time.Location) (string, int64) { return name, int64(offset) } + +// WithinDayTimePeriod tests whether `now` is between `start` and `end`. +func WithinDayTimePeriod(start, end, now time.Time) bool { + // Converts to UTC and only keeps the hour and minute info. + start, end, now = start.UTC(), end.UTC(), now.UTC() + start = time.Date(0, 0, 0, start.Hour(), start.Minute(), 0, 0, time.UTC) + end = time.Date(0, 0, 0, end.Hour(), end.Minute(), 0, 0, time.UTC) + now = time.Date(0, 0, 0, now.Hour(), now.Minute(), 0, 0, time.UTC) + // for cases like from 00:00 to 06:00 + if end.Sub(start) >= 0 { + return now.Sub(start) >= 0 && now.Sub(end) <= 0 + } + // for cases like from 22:00 to 06:00 + return now.Sub(end) <= 0 || now.Sub(start) >= 0 +}