Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bindinfo: add timeout for loading binding from storage #51550

Merged
merged 18 commits into from
Mar 12, 2024
797 changes: 581 additions & 216 deletions MODULE.bazel.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pkg/bindinfo/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ go_library(
"//pkg/util/chunk",
"//pkg/util/hack",
"//pkg/util/hint",
"//pkg/util/intest",
"//pkg/util/kvcache",
"//pkg/util/logutil",
"//pkg/util/mathutil",
"//pkg/util/memory",
"//pkg/util/parser",
"//pkg/util/sqlexec",
"//pkg/util/stmtsummary/v2:stmtsummary",
"//pkg/util/stringutil",
"//pkg/util/table-filter",
"@com_github_ngaut_pools//:pools",
"@com_github_pingcap_errors//:errors",
Expand Down
53 changes: 42 additions & 11 deletions pkg/bindinfo/binding_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,36 @@ package bindinfo
import (
"errors"
"sync"
"sync/atomic"
"time"

"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/pkg/bindinfo/norm"
"github.com/pingcap/tidb/pkg/parser"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/sessionctx"
"github.com/pingcap/tidb/pkg/sessionctx/variable"
"github.com/pingcap/tidb/pkg/util/hack"
"github.com/pingcap/tidb/pkg/util/intest"
"github.com/pingcap/tidb/pkg/util/kvcache"
"github.com/pingcap/tidb/pkg/util/logutil"
"github.com/pingcap/tidb/pkg/util/mathutil"
"github.com/pingcap/tidb/pkg/util/memory"
"github.com/pingcap/tidb/pkg/util/stringutil"
"go.uber.org/zap"
)

// GetBindingReturnNil is only for test
var GetBindingReturnNil = stringutil.StringerStr("GetBindingReturnNil")

// GetBindingReturnNilBool is only for test
var GetBindingReturnNilBool atomic.Bool

// GetBindingReturnNilAlways is only for test
var GetBindingReturnNilAlways = stringutil.StringerStr("getBindingReturnNilAlways")

// LoadBindingNothing is only for test
var LoadBindingNothing = stringutil.StringerStr("LoadBindingNothing")

// FuzzyBindingCache is based on BindingCache, and provide some more advanced features, like
// fuzzy matching, loading binding if cache miss automatically (TODO).
type FuzzyBindingCache interface {
Expand All @@ -57,10 +72,10 @@ type fuzzyBindingCache struct {
sql2FuzzyDigest map[string]string // sqlDigest --> fuzzyDigest

// loadBindingFromStorageFunc is used to load binding from storage if cache miss.
loadBindingFromStorageFunc func(sqlDigest string) (Bindings, error)
loadBindingFromStorageFunc func(sctx sessionctx.Context, sqlDigest string) (Bindings, error)
}

func newFuzzyBindingCache(loadBindingFromStorageFunc func(string) (Bindings, error)) FuzzyBindingCache {
func newFuzzyBindingCache(loadBindingFromStorageFunc func(sessionctx.Context, string) (Bindings, error)) FuzzyBindingCache {
return &fuzzyBindingCache{
BindingCache: newBindCache(),
fuzzy2SQLDigests: make(map[string][]string),
Expand All @@ -77,7 +92,7 @@ func (fbc *fuzzyBindingCache) FuzzyMatchingBinding(sctx sessionctx.Context, fuzz
if fbc.loadBindingFromStorageFunc == nil {
return
}
fbc.loadFromStore(missingSQLDigest) // loadFromStore's SetBinding has a Mutex inside, so it's safe to call it without lock
fbc.loadFromStore(sctx, missingSQLDigest) // loadFromStore's SetBinding has a Mutex inside, so it's safe to call it without lock
matchedBinding, isMatched, _ = fbc.getFromMemory(sctx, fuzzyDigest, tableNames)
return
}
Expand All @@ -92,7 +107,18 @@ func (fbc *fuzzyBindingCache) getFromMemory(sctx sessionctx.Context, fuzzyDigest
leastWildcards := len(tableNames) + 1
enableFuzzyBinding := sctx.GetSessionVars().EnableFuzzyBinding
for _, sqlDigest := range fbc.fuzzy2SQLDigests[fuzzyDigest] {
if bindings := bindingCache.GetBinding(sqlDigest); bindings != nil {
bindings := bindingCache.GetBinding(sqlDigest)
if intest.InTest {
if sctx.Value(GetBindingReturnNil) != nil {
if GetBindingReturnNilBool.CompareAndSwap(false, true) {
bindings = nil
}
}
if sctx.Value(GetBindingReturnNilAlways) != nil {
bindings = nil
}
}
if bindings != nil {
for _, binding := range bindings {
numWildcards, matched := fuzzyMatchBindingTableName(sctx.GetSessionVars().CurrentDB, tableNames, binding.TableNames)
if matched && numWildcards > 0 && sctx != nil && !enableFuzzyBinding {
Expand All @@ -112,11 +138,19 @@ func (fbc *fuzzyBindingCache) getFromMemory(sctx sessionctx.Context, fuzzyDigest
return matchedBinding, isMatched, missingSQLDigest
}

func (fbc *fuzzyBindingCache) loadFromStore(missingSQLDigest []string) {
func (fbc *fuzzyBindingCache) loadFromStore(sctx sessionctx.Context, missingSQLDigest []string) {
if intest.InTest && sctx.Value(LoadBindingNothing) != nil {
return
}
for _, sqlDigest := range missingSQLDigest {
bindings, err := fbc.loadBindingFromStorageFunc(sqlDigest)
start := time.Now()
bindings, err := fbc.loadBindingFromStorageFunc(sctx, sqlDigest)
if err != nil {
logutil.BgLogger().Warn("loadBindingFromStorageFunc binding failed", zap.String("sqlDigest", sqlDigest), zap.Error(err))
logutil.BgLogger().Warn("failed to load binding from storage",
zap.String("sqlDigest", sqlDigest),
zap.Error(err),
zap.Duration("duration", time.Since(start)),
)
continue
}
// put binding into the cache
Expand Down Expand Up @@ -317,9 +351,6 @@ func (c *bindingCache) delete(key bindingCacheKey) bool {
// The return value is not read-only, but it shouldn't be changed in the caller functions.
// The function is thread-safe.
func (c *bindingCache) GetBinding(sqlDigest string) Bindings {
failpoint.Inject("get_binding_return_nil", func(_ failpoint.Value) {
failpoint.Return(nil)
})
c.lock.Lock()
defer c.lock.Unlock()
return c.get(bindingCacheKey(sqlDigest))
Expand Down
4 changes: 3 additions & 1 deletion pkg/bindinfo/binding_match.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ func matchSQLBinding(sctx sessionctx.Context, stmtNode ast.StmtNode, info *Bindi
if globalHandle == nil {
return
}
if binding, matched := globalHandle.MatchGlobalBinding(sctx, fuzzyDigest, tableNames); matched {
binding, matched = globalHandle.MatchGlobalBinding(sctx, fuzzyDigest, tableNames)
if matched {
return binding, matched, metrics.ScopeGlobal
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

means return binding, matched, nil at last?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, actually there have been no changes here.

return
}

Expand Down
32 changes: 22 additions & 10 deletions pkg/bindinfo/global_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"time"

"github.com/pingcap/errors"
"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/pkg/parser"
"github.com/pingcap/tidb/pkg/parser/ast"
"github.com/pingcap/tidb/pkg/parser/format"
Expand Down Expand Up @@ -685,27 +686,38 @@ func (h *globalBindingHandle) Stats(_ *variable.SessionVars) (map[string]any, er
}

// LoadBindingsFromStorageToCache loads global bindings from storage to the memory cache.
func (h *globalBindingHandle) LoadBindingsFromStorage(sqlDigest string) (Bindings, error) {
func (h *globalBindingHandle) LoadBindingsFromStorage(sctx sessionctx.Context, sqlDigest string) (Bindings, error) {
if sqlDigest == "" {
return nil, nil
}
bindings, err, _ := h.syncBindingSingleflight.Do(sqlDigest, func() (any, error) {
timeout := time.Duration(sctx.GetSessionVars().LoadBindingTimeout) * time.Millisecond
resultChan := h.syncBindingSingleflight.DoChan(sqlDigest, func() (any, error) {
return h.loadBindingsFromStorageInternal(sqlDigest)
})
if err != nil {
logutil.BgLogger().Warn("fail to LoadBindingsFromStorageToCache", zap.Error(err))
return nil, err
}
if bindings == nil {
return nil, nil
select {
case result := <-resultChan:
if result.Err != nil {
return nil, result.Err
}
bindings := result.Val
if bindings == nil {
return nil, nil
}
return bindings.(Bindings), nil
case <-time.After(timeout):
return nil, errors.New("load bindings from storage timeout")
}
return bindings.(Bindings), err
}

func (h *globalBindingHandle) loadBindingsFromStorageInternal(sqlDigest string) (any, error) {
failpoint.Inject("load_bindings_from_storage_internal_timeout", func() {
time.Sleep(time.Second)
})

var bindings Bindings
selectStmt := fmt.Sprintf("SELECT original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest FROM mysql.bind_info where sql_digest = '%s'", sqlDigest)
err := h.callWithSCtx(false, func(sctx sessionctx.Context) error {
rows, _, err := execRows(sctx, "SELECT original_sql, bind_sql, default_db, status, create_time, update_time, charset, collation, source, sql_digest, plan_digest FROM mysql.bind_info where sql_digest = ?", sqlDigest)
rows, _, err := execRows(sctx, selectStmt)
if err != nil {
return err
}
Expand Down
1 change: 0 additions & 1 deletion pkg/bindinfo/global_handle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,6 @@ func TestGlobalBinding(t *testing.T) {
_, fuzzyDigest = norm.NormalizeStmtForBinding(stmt, norm.WithFuzz(true))
_, matched = dom.BindHandle().MatchGlobalBinding(tk.Session(), fuzzyDigest, bindinfo.CollectTableNames(stmt))
require.False(t, matched) // dropped

bindHandle = bindinfo.NewGlobalBindingHandle(&mockSessionPool{tk.Session()})
err = bindHandle.LoadFromStorageToCache(true)
require.NoError(t, err)
Expand Down
3 changes: 2 additions & 1 deletion pkg/bindinfo/session_handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ func (h *sessionBindingHandle) DropSessionBinding(sqlDigest string) error {

// MatchSessionBinding returns the matched binding for this statement.
func (h *sessionBindingHandle) MatchSessionBinding(sctx sessionctx.Context, fuzzyDigest string, tableNames []*ast.TableName) (matchedBinding Binding, isMatched bool) {
return h.ch.FuzzyMatchingBinding(sctx, fuzzyDigest, tableNames)
matchedBinding, isMatched = h.ch.FuzzyMatchingBinding(sctx, fuzzyDigest, tableNames)
return
}

// GetAllSessionBindings return all session bind info.
Expand Down
1 change: 0 additions & 1 deletion pkg/bindinfo/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ go_test(
"//pkg/util",
"//pkg/util/parser",
"//pkg/util/stmtsummary",
"@com_github_pingcap_failpoint//:failpoint",
"@com_github_stretchr_testify//require",
"@org_uber_go_goleak//:goleak",
],
Expand Down
7 changes: 4 additions & 3 deletions pkg/bindinfo/tests/bind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"strconv"
"testing"

"github.com/pingcap/failpoint"
"github.com/pingcap/tidb/pkg/bindinfo"
"github.com/pingcap/tidb/pkg/bindinfo/internal"
"github.com/pingcap/tidb/pkg/bindinfo/norm"
Expand Down Expand Up @@ -1099,13 +1098,15 @@ func TestFuzzyBindingHintsWithSourceReturning(t *testing.T) {
for _, currentDB := range []string{"db1", "db2", "db3"} {
tk.MustExec(`use ` + currentDB)
for _, db := range []string{"db1.", "db2.", "db3.", ""} {
require.NoError(t, failpoint.Enable("github.com/pingcap/tidb/pkg/bindinfo/get_binding_return_nil", `return()`))
query := fmt.Sprintf(c.qTemplate, db)
tk.MustExec(query)
require.NoError(t, failpoint.Disable("github.com/pingcap/tidb/pkg/bindinfo/get_binding_return_nil"))
tk.MustQuery(`show warnings`).Check(testkit.Rows()) // no warning
sctx := tk.Session()
sctx.SetValue(bindinfo.GetBindingReturnNil, true)
tk.MustExec(query)
sctx.ClearValue(bindinfo.GetBindingReturnNil)
tk.MustQuery(`select @@last_plan_from_binding`).Check(testkit.Rows("1"))
bindinfo.GetBindingReturnNilBool.Store(false)
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions pkg/bindinfo/tests/timeout/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")

go_test(
name = "timeout_test",
timeout = "short",
srcs = [
"main_test.go",
"timeout_test.go",
],
flaky = True,
deps = [
"//pkg/bindinfo",
"//pkg/testkit",
"//pkg/testkit/testsetup",
"@org_uber_go_goleak//:goleak",
],
)
35 changes: 35 additions & 0 deletions pkg/bindinfo/tests/timeout/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2024 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package timeout

import (
"testing"

"github.com/pingcap/tidb/pkg/testkit/testsetup"
"go.uber.org/goleak"
)

func TestMain(m *testing.M) {
testsetup.SetupForCommonTest()
opts := []goleak.Option{
goleak.IgnoreTopFunction("github.com/golang/glog.(*fileSink).flushDaemon"),
goleak.IgnoreTopFunction("github.com/bazelbuild/rules_go/go/tools/bzltestutil.RegisterTimeoutHandler.func1"),
goleak.IgnoreTopFunction("github.com/lestrrat-go/httprc.runFetchWorker"),
goleak.IgnoreTopFunction("go.etcd.io/etcd/client/pkg/v3/logutil.(*MergeLogger).outputLoop"),
goleak.IgnoreTopFunction("go.opencensus.io/stats/view.(*worker).start"),
goleak.IgnoreTopFunction("time.Sleep"),
}
goleak.VerifyTestMain(m, opts...)
}
67 changes: 67 additions & 0 deletions pkg/bindinfo/tests/timeout/timeout_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2024 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package timeout

import (
"fmt"
"testing"

"github.com/pingcap/tidb/pkg/bindinfo"
"github.com/pingcap/tidb/pkg/testkit"
)

func TestFuzzyBindingHintsWithSourceReturningTimeout(t *testing.T) {
store := testkit.CreateMockStore(t)
tk := testkit.NewTestKit(t, store)
tk.MustExec(`use test`)

for _, db := range []string{"db1", "db2", "db3"} {
tk.MustExec(`create database ` + db)
tk.MustExec(`use ` + db)
tk.MustExec(`create table t1 (a int, b int, c int, d int, key(a), key(b), key(c), key(d))`)
tk.MustExec(`create table t2 (a int, b int, c int, d int, key(a), key(b), key(c), key(d))`)
tk.MustExec(`create table t3 (a int, b int, c int, d int, key(a), key(b), key(c), key(d))`)
}
tk.MustExec(`set @@tidb_opt_enable_fuzzy_binding=1`)

for _, c := range []struct {
binding string
qTemplate string
}{
// use index
{`create global binding using select /*+ use_index(t1, c) */ * from *.t1 where a=1`,
`select * from %st1 where a=1000`},
} {
tk.MustExec(c.binding)
for _, currentDB := range []string{"db1", "db2", "db3"} {
tk.MustExec(`use ` + currentDB)
for _, db := range []string{"db1.", "db2.", "db3.", ""} {
query := fmt.Sprintf(c.qTemplate, db)
tk.MustExec(query)
tk.MustQuery(`show warnings`).Check(testkit.Rows()) // no warning
sctx := tk.Session()
sctx.SetValue(bindinfo.GetBindingReturnNil, true)
sctx.SetValue(bindinfo.GetBindingReturnNilAlways, true)
sctx.SetValue(bindinfo.LoadBindingNothing, true)
tk.MustExec(query)
sctx.ClearValue(bindinfo.GetBindingReturnNil)
sctx.ClearValue(bindinfo.GetBindingReturnNilAlways)
sctx.ClearValue(bindinfo.LoadBindingNothing)
tk.MustQuery(`select @@last_plan_from_binding`).Check(testkit.Rows("0"))
bindinfo.GetBindingReturnNilBool.Store(false)
}
}
}
}
3 changes: 3 additions & 0 deletions pkg/sessionctx/variable/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,9 @@ type SessionVars struct {
// See https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_execution_time
MaxExecutionTime uint64

// LoadBindingTimeout is the timeout for loading the bind info.
LoadBindingTimeout uint64

// TiKVClientReadTimeout is the timeout for readonly kv request in milliseconds, 0 means using default value
// See https://github.com/pingcap/tidb/blob/7105505a78fc886c33258caa5813baf197b15247/docs/design/2023-06-30-configurable-kv-timeout.md?plain=1#L14-L15
TiKVClientReadTimeout uint64
Expand Down
Loading