Skip to content

Commit 486250f

Browse files
committed
planner: add delete-by-key support for instance plan cache
1 parent dcf6f03 commit 486250f

3 files changed

Lines changed: 103 additions & 17 deletions

File tree

pkg/planner/core/plan_cache_instance.go

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ type instancePCNode struct {
4343
value *PlanCacheValue
4444
lastUsed atomic.Time
4545
next atomic.Pointer[instancePCNode]
46+
deleted atomic.Bool
4647
}
4748

49+
var deletedInstancePCNode = &instancePCNode{}
50+
4851
// instancePlanCache is a lock-free implementation of InstancePlanCache interface.
4952
// [key1] --> [headNode1] --> [node1] --> [node2] --> [node3]
5053
// [key2] --> [headNode2] --> [node4] --> [node5]
@@ -62,32 +65,45 @@ type instancePlanCache struct {
6265
}
6366

6467
func (pc *instancePlanCache) getHead(key string, create bool) *instancePCNode {
65-
headNode, ok := pc.heads.Load(key)
66-
if ok { // cache hit
67-
return headNode.(*instancePCNode)
68-
}
69-
if !create { // cache miss
70-
return nil
71-
}
72-
newHeadNode := pc.createNode(nil)
73-
actual, _ := pc.heads.LoadOrStore(key, newHeadNode)
74-
if headNode, ok := actual.(*instancePCNode); ok { // for safety
75-
return headNode
68+
for {
69+
headNode, ok := pc.heads.Load(key)
70+
if ok { // cache hit
71+
head := headNode.(*instancePCNode)
72+
if !head.deleted.Load() {
73+
return head
74+
}
75+
if !create {
76+
return nil
77+
}
78+
pc.heads.CompareAndDelete(key, headNode)
79+
continue
80+
}
81+
if !create { // cache miss
82+
return nil
83+
}
84+
newHeadNode := pc.createNode(nil)
85+
actual, loaded := pc.heads.LoadOrStore(key, newHeadNode)
86+
if !loaded {
87+
return newHeadNode
88+
}
89+
if headNode, ok := actual.(*instancePCNode); ok && !headNode.deleted.Load() { // for safety
90+
return headNode
91+
}
92+
pc.heads.CompareAndDelete(key, actual)
7693
}
77-
return nil
7894
}
7995

8096
// Get gets the cached value according to key and paramTypes.
8197
func (pc *instancePlanCache) Get(key string, paramTypes any) (value any, ok bool) {
8298
headNode := pc.getHead(key, false)
83-
if headNode == nil { // cache miss
99+
if headNode == nil || headNode.deleted.Load() { // cache miss
84100
return nil, false
85101
}
86102
return pc.getPlanFromList(headNode, paramTypes)
87103
}
88104

89105
func (pc *instancePlanCache) getPlanFromList(headNode *instancePCNode, paramTypes any) (any, bool) {
90-
for node := headNode.next.Load(); node != nil; node = node.next.Load() {
106+
for node := headNode.next.Load(); node != nil && node != deletedInstancePCNode; node = node.next.Load() {
91107
if checkTypesCompatibility4PC(node.value.ParamTypes, paramTypes) { // v.Plan is read-only, no need to lock
92108
if !pc.inEvict.Load() {
93109
node.lastUsed.Store(time.Now()) // atomically update the lastUsed field
@@ -109,17 +125,20 @@ func (pc *instancePlanCache) Put(key string, value, paramTypes any) (succ bool)
109125
return // do nothing if it exceeds the hard limit
110126
}
111127
headNode := pc.getHead(key, true)
112-
if headNode == nil {
128+
if headNode == nil || headNode.deleted.Load() {
113129
return false // for safety
114130
}
115131
if _, ok := pc.getPlanFromList(headNode, paramTypes); ok {
116132
return // some other thread has inserted the same plan before
117133
}
118-
if pc.inEvict.Load() {
134+
if pc.inEvict.Load() || headNode.deleted.Load() {
119135
return // do nothing if eviction is in progress
120136
}
121137

122138
firstNode := headNode.next.Load()
139+
if firstNode == deletedInstancePCNode {
140+
return
141+
}
123142
currNode := pc.createNode(value)
124143
currNode.next.Store(firstNode)
125144
if headNode.next.CompareAndSwap(firstNode, currNode) { // if failed, some other thread has updated this node,
@@ -130,6 +149,28 @@ func (pc *instancePlanCache) Put(key string, value, paramTypes any) (succ bool)
130149
return
131150
}
132151

152+
// Delete removes all cached values under the exact cache key.
153+
func (pc *instancePlanCache) Delete(key string) (numDeleted int) {
154+
pc.evictMutex.Lock() // serialize against Evict and key deletion for safety
155+
defer pc.evictMutex.Unlock()
156+
pc.inEvict.Store(true)
157+
defer pc.inEvict.Store(false)
158+
159+
headNode := pc.getHead(key, false)
160+
if headNode == nil {
161+
return 0
162+
}
163+
headNode.deleted.Store(true)
164+
firstNode := headNode.next.Swap(deletedInstancePCNode)
165+
pc.heads.Delete(key)
166+
for node := firstNode; node != nil && node != deletedInstancePCNode; node = node.next.Load() {
167+
pc.totCost.Sub(node.value.MemoryUsage())
168+
pc.totPlan.Sub(1)
169+
numDeleted++
170+
}
171+
return
172+
}
173+
133174
// All returns all cached values.
134175
// All returned values are read-only, don't modify them.
135176
func (pc *instancePlanCache) All() (values []any) {
@@ -226,7 +267,7 @@ func (pc *instancePlanCache) calcEvictionThreshold(lastUsedTimes []time.Time) (t
226267
func (pc *instancePlanCache) foreach(callback func(prev, this *instancePCNode) (thisRemoved bool)) {
227268
_, headNodes := pc.headNodes()
228269
for _, headNode := range headNodes {
229-
for prev, this := headNode, headNode.next.Load(); this != nil; {
270+
for prev, this := headNode, headNode.next.Load(); this != nil && this != deletedInstancePCNode; {
230271
thisRemoved := callback(prev, this)
231272
if !thisRemoved { // this node is removed, no need to update the prev node in this case
232273
prev, this = this, this.next.Load()

pkg/planner/core/plan_cache_instance_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ import (
2323
"time"
2424

2525
"github.com/pingcap/tidb/pkg/domain"
26+
"github.com/pingcap/tidb/pkg/parser/mysql"
2627
"github.com/pingcap/tidb/pkg/planner/util/coretestsdk"
2728
"github.com/pingcap/tidb/pkg/sessionctx"
29+
"github.com/pingcap/tidb/pkg/types"
2830
"github.com/stretchr/testify/require"
2931
)
3032

@@ -44,6 +46,11 @@ func _miss(t *testing.T, pc sessionctx.InstancePlanCache, testKey, statsHash int
4446
require.False(t, ok)
4547
}
4648

49+
func _putWithParamTypes(pc sessionctx.InstancePlanCache, key string, memUsage int64, paramTypes []*types.FieldType) (succ bool) {
50+
v := &PlanCacheValue{Memory: memUsage, ParamTypes: paramTypes}
51+
return pc.Put(key, v, paramTypes)
52+
}
53+
4754
func TestInstancePlanCacheBasic(t *testing.T) {
4855
sctx := coretestsdk.MockContext()
4956
defer func() {
@@ -75,6 +82,20 @@ func TestInstancePlanCacheBasic(t *testing.T) {
7582
_put(pc, 1, 101, 0)
7683
require.Equal(t, pc.MemUsage(), int64(100)) // the second one will be ignored
7784

85+
// delete an exact key
86+
pc = NewInstancePlanCache(1000, 1000)
87+
_put(pc, 1, 100, 0)
88+
_put(pc, 2, 100, 0)
89+
_put(pc, 3, 100, 0)
90+
numDeleted := pc.Delete("2-0")
91+
require.Equal(t, 1, numDeleted)
92+
require.Equal(t, int64(200), pc.MemUsage())
93+
require.Equal(t, int64(2), pc.Size())
94+
_hit(t, pc, 1, 0)
95+
_miss(t, pc, 2, 0)
96+
_hit(t, pc, 3, 0)
97+
require.Equal(t, 0, pc.Delete("not-exist"))
98+
7899
// eviction
79100
pc = NewInstancePlanCache(320, 500)
80101
_put(pc, 1, 100, 0)
@@ -158,6 +179,28 @@ func TestInstancePlanCacheWithMatchOpts(t *testing.T) {
158179
_miss(t, pc, 3, 2)
159180
_miss(t, pc, 3, 3)
160181

182+
// same exact key with different param types should be deleted together
183+
pc = NewInstancePlanCache(1000, 1000)
184+
key := "shared-key"
185+
paramTypes1 := []*types.FieldType{types.NewFieldType(mysql.TypeLonglong)}
186+
paramTypes2 := []*types.FieldType{types.NewFieldType(mysql.TypeDouble)}
187+
require.True(t, _putWithParamTypes(pc, key, 100, paramTypes1))
188+
require.True(t, _putWithParamTypes(pc, key, 100, paramTypes2))
189+
_put(pc, 2, 100, 1)
190+
_, ok := pc.Get(key, paramTypes1)
191+
require.True(t, ok)
192+
_, ok = pc.Get(key, paramTypes2)
193+
require.True(t, ok)
194+
numDeleted := pc.Delete(key)
195+
require.Equal(t, 2, numDeleted)
196+
require.Equal(t, int64(100), pc.MemUsage())
197+
require.Equal(t, int64(1), pc.Size())
198+
_, ok = pc.Get(key, paramTypes1)
199+
require.False(t, ok)
200+
_, ok = pc.Get(key, paramTypes2)
201+
require.False(t, ok)
202+
_hit(t, pc, 2, 1)
203+
161204
// hard limit can take effect in this case
162205
pc = NewInstancePlanCache(200, 200)
163206
_put(pc, 1, 100, 1)

pkg/sessionctx/context.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ type InstancePlanCache interface {
6161
Get(key string, paramTypes any) (value any, ok bool)
6262
// Put puts the key and value into the cache.
6363
Put(key string, value, paramTypes any) (succ bool)
64+
// Delete removes all cached values under the exact cache key.
65+
Delete(key string) (numDeleted int)
6466
// All returns all cached values.
6567
// Returned values are read-only, don't modify them.
6668
All() (values []any)

0 commit comments

Comments
 (0)