Skip to content

Commit 9578f83

Browse files
committed
tree: ensure that InjectHints is thread-safe
By re-using the same `collapsed` stack for both walks, InjectHints is not safe to call from multiple goroutines. We need to be able to safely call InjectHints from multiple sessions for the same donor. To fix this, use separate `collapsed` stacks for both walks. Informs: #153633 Release note: None
1 parent 435e9d4 commit 9578f83

File tree

1 file changed

+60
-39
lines changed

1 file changed

+60
-39
lines changed

pkg/sql/sem/tree/inject_hints.go

Lines changed: 60 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -28,32 +28,39 @@ type HintInjectionDonor struct {
2828
// in the same order. Some nodes are ignored, such as ParenExpr and nodes
2929
// after collapse of a Tuple or Array or ValuesClause.
3030
walk []any
31-
32-
// collapsed is a stack indicating whether we've collapsed the rest of the
33-
// current ValuesClause, Tuple, or Array list in the donor AST. Collapsed is
34-
// only used during AST walks.
35-
collapsed []bool
3631
}
3732

3833
// NewHintInjectionDonor creates a HintInjectionDonor from a parsed AST. The
3934
// parsed donor statement could be a regular SQL statement or a statement
4035
// fingerprint.
36+
//
37+
// After NewHintInjectionDonor returns a HintInjectionDonor, the donor becomes
38+
// read-only to allow concurrent use from multiple goroutines.
4139
func NewHintInjectionDonor(ast Statement, fingerprintFlags FmtFlags) (*HintInjectionDonor, error) {
4240
hd := &HintInjectionDonor{
4341
ast: ast,
4442
validationSQL: FormatStatementHideConstants(ast, fingerprintFlags, FmtHideHints),
45-
collapsed: []bool{false},
4643
}
47-
WalkStmt(hd, ast)
44+
v := newDonorVisitor{hd: hd, collapsed: []bool{false}}
45+
WalkStmt(&v, ast)
4846
return hd, nil
4947
}
5048

51-
// The ExtendedVisitor methods of HintInjectionDonor are used to walk the donor
52-
// AST and build the ahead-of-time pre-order traversal.
53-
var _ ExtendedVisitor = &HintInjectionDonor{}
49+
// newDonorVisitor is an ExtendedVisitor used to walk the donor AST and build
50+
// the ahead-of-time pre-order traversal at donor creation time.
51+
type newDonorVisitor struct {
52+
// hd is the hint donor being initialized.
53+
hd *HintInjectionDonor
54+
55+
// collapsed is a stack indicating whether we've collapsed the rest of the
56+
// current ValuesClause, Tuple, or Array list in the donor AST.
57+
collapsed []bool
58+
}
59+
60+
var _ ExtendedVisitor = &newDonorVisitor{}
5461

55-
func (hd *HintInjectionDonor) VisitPre(expr Expr) (recurse bool, newExpr Expr) {
56-
if hd.collapsed[len(hd.collapsed)-1] {
62+
func (v *newDonorVisitor) VisitPre(expr Expr) (recurse bool, newExpr Expr) {
63+
if v.collapsed[len(v.collapsed)-1] {
5764
return false, expr
5865
}
5966
switch expr.(type) {
@@ -65,78 +72,88 @@ func (hd *HintInjectionDonor) VisitPre(expr Expr) (recurse bool, newExpr Expr) {
6572
// If the donor was a statement fingerprint, we might have collapse a Tuple
6673
// or Array into __more__. Push on a boolean tracking whether we have
6774
// collapsed it.
68-
hd.collapsed = append(hd.collapsed, false)
75+
v.collapsed = append(v.collapsed, false)
6976
case *UnresolvedName:
7077
if isArityIndicatorString(expr.String()) {
7178
// If we find __more__ or another arity indicator, treat the rest of the
7279
// current Tuple or Array or ValuesClause as collapsed, and skip forward
7380
// until we VisitPost it.
74-
hd.collapsed[len(hd.collapsed)-1] = true
75-
hd.walk = append(hd.walk, expr)
81+
v.collapsed[len(v.collapsed)-1] = true
82+
v.hd.walk = append(v.hd.walk, expr)
7683
return false, expr
7784
}
7885
}
79-
hd.walk = append(hd.walk, expr)
86+
v.hd.walk = append(v.hd.walk, expr)
8087
return true, expr
8188
}
8289

83-
func (hd *HintInjectionDonor) VisitPost(expr Expr) (newNode Expr) {
90+
func (v *newDonorVisitor) VisitPost(expr Expr) (newNode Expr) {
8491
switch expr.(type) {
8592
case *Tuple, *Array:
8693
// Pop off the boolean tracking whether we collapsed the Tuple or Array.
87-
hd.collapsed = hd.collapsed[:len(hd.collapsed)-1]
94+
v.collapsed = v.collapsed[:len(v.collapsed)-1]
8895
}
8996
return expr
9097
}
9198

92-
func (hd *HintInjectionDonor) VisitTablePre(expr TableExpr) (recurse bool, newExpr TableExpr) {
93-
if hd.collapsed[len(hd.collapsed)-1] {
99+
func (v *newDonorVisitor) VisitTablePre(expr TableExpr) (recurse bool, newExpr TableExpr) {
100+
if v.collapsed[len(v.collapsed)-1] {
94101
return false, expr
95102
}
96-
hd.walk = append(hd.walk, expr)
103+
v.hd.walk = append(v.hd.walk, expr)
97104
return true, expr
98105
}
99106

100-
func (hd *HintInjectionDonor) VisitTablePost(expr TableExpr) (newNode TableExpr) {
107+
func (v *newDonorVisitor) VisitTablePost(expr TableExpr) (newNode TableExpr) {
101108
return expr
102109
}
103110

104-
func (hd *HintInjectionDonor) VisitStatementPre(expr Statement) (recurse bool, newNode Statement) {
105-
if hd.collapsed[len(hd.collapsed)-1] {
111+
func (v *newDonorVisitor) VisitStatementPre(expr Statement) (recurse bool, newNode Statement) {
112+
if v.collapsed[len(v.collapsed)-1] {
106113
return false, expr
107114
}
108115
switch expr.(type) {
109116
case *ValuesClause:
110117
// If the donor was a statement fingerprint, we might have collapse a
111118
// ValuesClause into __more__. Push on a boolean tracking whether we have
112119
// collapsed it.
113-
hd.collapsed = append(hd.collapsed, false)
120+
v.collapsed = append(v.collapsed, false)
114121
}
115-
hd.walk = append(hd.walk, expr)
122+
v.hd.walk = append(v.hd.walk, expr)
116123
return true, expr
117124
}
118125

119-
func (hd *HintInjectionDonor) VisitStatementPost(expr Statement) (newNode Statement) {
126+
func (v *newDonorVisitor) VisitStatementPost(expr Statement) (newNode Statement) {
120127
switch expr.(type) {
121128
case *ValuesClause:
122129
// Pop off the boolean tracking whether we collapsed the ValuesClause.
123-
hd.collapsed = hd.collapsed[:len(hd.collapsed)-1]
130+
v.collapsed = v.collapsed[:len(v.collapsed)-1]
124131
}
125132
return expr
126133
}
127134

128135
// hintInjectionVisitor is an ExtendedVisitor used to walk the target AST at
129136
// injection time. It performs the rewrite of the target AST.
130137
type hintInjectionVisitor struct {
131-
hd *HintInjectionDonor
138+
// hd is the hint donor. Note that multiple hintInjectionVisitors could share
139+
// the same donor concurrently from different sessions, so this is read-only.
140+
hd *HintInjectionDonor
141+
142+
// walkIdx is the current position within the donor AST walk.
132143
walkIdx int
133-
err error
144+
145+
// collapsed is a stack indicating whether we've collapsed the rest of the
146+
// current ValuesClause, Tuple, or Array list in the donor AST.
147+
collapsed []bool
148+
149+
// err is set if we detect a mismatch while walking the target AST.
150+
err error
134151
}
135152

136153
var _ ExtendedVisitor = &hintInjectionVisitor{}
137154

138155
func (v *hintInjectionVisitor) VisitPre(expr Expr) (recurse bool, newExpr Expr) {
139-
if v.err != nil || v.hd.collapsed[len(v.hd.collapsed)-1] {
156+
if v.err != nil || v.collapsed[len(v.collapsed)-1] {
140157
return false, expr
141158
}
142159

@@ -161,7 +178,7 @@ func (v *hintInjectionVisitor) VisitPre(expr Expr) (recurse bool, newExpr Expr)
161178
// If the donor was a statement fingerprint, we might have collapse a Tuple
162179
// or Array into __more__. Push on a boolean tracking whether we have
163180
// collapsed it.
164-
v.hd.collapsed = append(v.hd.collapsed, false)
181+
v.collapsed = append(v.collapsed, false)
165182
case *UnresolvedName:
166183
// If the donor was a fingerprint, then we might not exactly match _ or
167184
// __more__ or __more10_100__ etc in the donor. Treat these as wildcards
@@ -172,7 +189,7 @@ func (v *hintInjectionVisitor) VisitPre(expr Expr) (recurse bool, newExpr Expr)
172189
if isArityIndicatorString(donor.String()) {
173190
// And skip the rest of the current Tuple or Array or ValuesClause if it
174191
// has been collapsed to __more__ or one of the other arity indicators.
175-
v.hd.collapsed[len(v.hd.collapsed)-1] = true
192+
v.collapsed[len(v.collapsed)-1] = true
176193
return false, expr
177194
}
178195
}
@@ -191,13 +208,13 @@ func (v *hintInjectionVisitor) VisitPost(expr Expr) Expr {
191208
switch expr.(type) {
192209
case *Tuple, *Array:
193210
// Pop off the boolean tracking whether we collapsed the Tuple or Array.
194-
v.hd.collapsed = v.hd.collapsed[:len(v.hd.collapsed)-1]
211+
v.collapsed = v.collapsed[:len(v.collapsed)-1]
195212
}
196213
return expr
197214
}
198215

199216
func (v *hintInjectionVisitor) VisitTablePre(expr TableExpr) (recurse bool, newExpr TableExpr) {
200-
if v.err != nil || v.hd.collapsed[len(v.hd.collapsed)-1] {
217+
if v.err != nil || v.collapsed[len(v.collapsed)-1] {
201218
return false, expr
202219
}
203220

@@ -266,7 +283,7 @@ func (v *hintInjectionVisitor) VisitTablePost(expr TableExpr) TableExpr {
266283
}
267284

268285
func (v *hintInjectionVisitor) VisitStatementPre(expr Statement) (recurse bool, newExpr Statement) {
269-
if v.err != nil || v.hd.collapsed[len(v.hd.collapsed)-1] {
286+
if v.err != nil || v.collapsed[len(v.collapsed)-1] {
270287
return false, expr
271288
}
272289

@@ -285,7 +302,7 @@ func (v *hintInjectionVisitor) VisitStatementPre(expr Statement) (recurse bool,
285302
// If the donor was a statement fingerprint, we might have collapse a
286303
// ValuesClause into __more__. Push on a boolean tracking whether we have
287304
// collapsed it.
288-
v.hd.collapsed = append(v.hd.collapsed, false)
305+
v.collapsed = append(v.collapsed, false)
289306
}
290307

291308
// Check that the corresponding donor Expr matches this node.
@@ -302,13 +319,15 @@ func (v *hintInjectionVisitor) VisitStatementPost(expr Statement) Statement {
302319
switch expr.(type) {
303320
case *ValuesClause:
304321
// Pop off the boolean tracking whether we collapsed the ValuesClause.
305-
v.hd.collapsed = v.hd.collapsed[:len(v.hd.collapsed)-1]
322+
v.collapsed = v.collapsed[:len(v.collapsed)-1]
306323
}
307324
return expr
308325
}
309326

310327
// Validate checks that the target statement exactly matches the donor (except
311328
// for hints).
329+
//
330+
// It is safe to call Validate concurrently from multiple goroutines.
312331
func (hd *HintInjectionDonor) Validate(stmt Statement, fingerprintFlags FmtFlags) error {
313332
sql := FormatStatementHideConstants(stmt, fingerprintFlags, FmtHideHints)
314333
if sql != hd.validationSQL {
@@ -332,9 +351,11 @@ func (hd *HintInjectionDonor) Validate(stmt Statement, fingerprintFlags FmtFlags
332351
//
333352
// No semantic checks of the donor hints are performed. It is assumed that
334353
// semantic checks happen elsewhere.
354+
//
355+
// It is safe to call InjectHints concurrently from multiple goroutines.
335356
func (hd *HintInjectionDonor) InjectHints(stmt Statement) (Statement, bool, error) {
336357
// Walk the given statement, copying hints from the hints donor.
337-
v := hintInjectionVisitor{hd: hd}
358+
v := hintInjectionVisitor{hd: hd, collapsed: []bool{false}}
338359
newStmt, changed := WalkStmt(&v, stmt)
339360
if v.err != nil {
340361
return stmt, false, v.err

0 commit comments

Comments
 (0)