Skip to content

Commit 784d8f9

Browse files
fix scanner pool consuming large memory (#22679)
fix scanner pool consuming large memory Approved by: @XuPeng-SH
1 parent bbabafe commit 784d8f9

File tree

2 files changed

+106
-3
lines changed

2 files changed

+106
-3
lines changed

pkg/sql/parsers/dialect/mysql/scanner.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ import (
2727

2828
const eofChar = 0x100
2929

30+
// maxPoolSQLSize is the size threshold beyond which a scanner will not be kept
31+
// in the pool and large fields will be cleared to release memory.
32+
const maxPoolSQLSize = 1 << 20 // 1 MiB
33+
3034
var scannerPool = sync.Pool{
3135
New: func() any {
3236
return &Scanner{}
@@ -50,18 +54,35 @@ type Scanner struct {
5054
strBuilder bytes.Buffer
5155
}
5256

53-
func (s *Scanner) setSql(sql string) {
54-
// This is a mysql scanner, so we set the dialect type to mysql
55-
s.dialectType = dialect.MYSQL
57+
func (s *Scanner) reset(clearLargeOnly bool, oversized bool) {
58+
// Reset light-weight state shared by both setSql and PutScanner
5659
s.LastToken = ""
5760
s.LastError = nil
5861
s.posVarIndex = 0
5962
s.MysqlSpecialComment = nil
63+
s.CommentFlag = false
6064
s.Pos = 0
6165
s.Line = 0
6266
s.Col = 0
6367
s.PrePos = 0
68+
69+
if clearLargeOnly {
70+
if oversized {
71+
// Oversized by SQL size: drop both to avoid retaining huge memory.
72+
s.buf = ""
73+
s.strBuilder = bytes.Buffer{}
74+
}
75+
}
76+
}
77+
78+
func (s *Scanner) setSql(sql string) {
79+
// This is a mysql scanner, so we set the dialect type to mysql
80+
s.dialectType = dialect.MYSQL
81+
// Reset transient fields but do not aggressively clear buffers here so that
82+
// small capacities can be reused.
83+
s.reset(false, false)
6484
s.buf = sql
85+
// Reset length to 0; this keeps capacity for small cases.
6586
s.strBuilder.Reset()
6687
}
6788

@@ -72,6 +93,12 @@ func NewScanner(dialectType dialect.DialectType, sql string) *Scanner {
7293
}
7394

7495
func PutScanner(scanner *Scanner) {
96+
oversized := len(scanner.buf) > maxPoolSQLSize
97+
// Reset shared state. Only clear buffers/strings when oversized.
98+
scanner.reset(true, oversized)
99+
if oversized {
100+
return
101+
}
75102
scannerPool.Put(scanner)
76103
}
77104

pkg/sql/parsers/dialect/mysql/scanner_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,79 @@ func TestHexadecimalLiteral(t *testing.T) {
391391
}
392392
}
393393
}
394+
395+
func TestScannerPoolCleanupAndThreshold(t *testing.T) {
396+
// Case 1: normal small SQL should be pooled and fields cleared
397+
s := NewScanner(dialect.MYSQL, "select 1")
398+
// grow strBuilder a little to ensure it is cleared on Put
399+
s.strBuilder.WriteString("abc")
400+
PutScanner(s)
401+
402+
// Fetch again to see if we receive a cleared scanner from pool
403+
s2 := NewScanner(dialect.MYSQL, "select 2")
404+
if s2.LastToken != "" || s2.LastError != nil || s2.MysqlSpecialComment != nil || s2.Pos != 0 || s2.Line != 0 || s2.Col != 0 || s2.PrePos != 0 {
405+
t.Fatalf("pooled scanner should be reset: %+v", s2)
406+
}
407+
if s2.strBuilder.Len() != 0 {
408+
t.Fatalf("strBuilder should be cleared")
409+
}
410+
PutScanner(s2)
411+
412+
// Case 2: big SQL (>1MiB) should NOT be pooled
413+
big := make([]byte, (1<<20)+10)
414+
for i := range big {
415+
big[i] = 'a'
416+
}
417+
sbig := NewScanner(dialect.MYSQL, string(big))
418+
// also grow internal builder to simulate expansion
419+
sbig.strBuilder.Grow(1 << 20)
420+
PutScanner(sbig)
421+
422+
// Next Get should not necessarily return the same oversized instance; at least, it must be a clean one
423+
s3 := NewScanner(dialect.MYSQL, "select 3")
424+
if s3.buf != "select 3" {
425+
t.Fatalf("unexpected scanner buf after Get")
426+
}
427+
PutScanner(s3)
428+
}
429+
430+
func TestPutScannerSmallKeepsBuffers(t *testing.T) {
431+
// Small SQL should keep buf and builder content when returned to pool
432+
sql := "select 1"
433+
s := NewScanner(dialect.MYSQL, sql)
434+
s.strBuilder.WriteString("xyz")
435+
PutScanner(s)
436+
437+
if s.buf == "" {
438+
t.Fatalf("small scanner buf should not be cleared on PutScanner")
439+
}
440+
if s.strBuilder.Len() == 0 {
441+
t.Fatalf("small scanner strBuilder should retain content on PutScanner")
442+
}
443+
444+
// When taking from pool next time, setSql will Reset the builder length
445+
s2 := NewScanner(dialect.MYSQL, "select 2")
446+
if s2.strBuilder.Len() != 0 {
447+
t.Fatalf("builder length must be reset on setSql")
448+
}
449+
PutScanner(s2)
450+
}
451+
452+
func TestPutScannerOversizedClearsBuffers(t *testing.T) {
453+
// Big SQL should be cleared and dropped
454+
big := make([]byte, (1<<20)+123)
455+
for i := range big {
456+
big[i] = 'b'
457+
}
458+
s := NewScanner(dialect.MYSQL, string(big))
459+
s.strBuilder.Grow(1 << 20)
460+
s.strBuilder.WriteString("payload")
461+
PutScanner(s)
462+
463+
if s.buf != "" {
464+
t.Fatalf("oversized scanner buf should be cleared on PutScanner")
465+
}
466+
if s.strBuilder.Len() != 0 {
467+
t.Fatalf("oversized scanner strBuilder should be zeroed on PutScanner")
468+
}
469+
}

0 commit comments

Comments
 (0)