github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/querycache/query_cache_test.go (about) 1 // Copyright 2018 The Cockroach Authors. 2 // 3 // Use of this software is governed by the Business Source License 4 // included in the file licenses/BSL.txt. 5 // 6 // As of the Change Date specified in that file, in accordance with 7 // the Business Source License, use of this software will be governed 8 // by the Apache License, Version 2.0, included in the file 9 // licenses/APL.txt. 10 11 package querycache 12 13 import ( 14 "fmt" 15 "strings" 16 "sync" 17 "testing" 18 19 "github.com/cockroachdb/cockroach/pkg/sql/opt/memo" 20 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 21 "github.com/cockroachdb/cockroach/pkg/util/randutil" 22 ) 23 24 func toStr(c *C) string { 25 c.check() 26 27 c.mu.Lock() 28 defer c.mu.Unlock() 29 30 var b strings.Builder 31 for e := c.mu.used.next; e != &c.mu.used; e = e.next { 32 if b.Len() != 0 { 33 b.WriteString(",") 34 } 35 b.WriteString(e.SQL) 36 } 37 return b.String() 38 } 39 40 func expect(t *testing.T, c *C, exp string) { 41 t.Helper() 42 if s := toStr(c); s != exp { 43 t.Errorf("expected %s, got %s", exp, s) 44 } 45 } 46 47 func data(sql string, mem *memo.Memo, memEstimate int64) *CachedData { 48 cd := &CachedData{SQL: sql, Memo: mem, PrepareMetadata: &sqlbase.PrepareMetadata{}} 49 n := memEstimate - cd.memoryEstimate() 50 if n < 0 { 51 panic(fmt.Sprintf("size %d too small", memEstimate)) 52 } 53 // Add characters to AnonymizedStr which should increase the estimate. 54 s := make([]byte, n) 55 for i := range s { 56 s[i] = 'x' 57 } 58 cd.PrepareMetadata.AnonymizedStr = string(s) 59 if cd.memoryEstimate() != memEstimate { 60 panic(fmt.Sprintf("failed to create CachedData of size %d", memEstimate)) 61 } 62 return cd 63 } 64 65 // TestCache tests the main operations of the cache. 66 func TestCache(t *testing.T) { 67 sa := &memo.Memo{} 68 sb := &memo.Memo{} 69 sc := &memo.Memo{} 70 sd := &memo.Memo{} 71 72 // In this test, all entries have the same size: avgCachedSize. 73 c := New(3 * avgCachedSize) 74 75 var s Session 76 s.Init() 77 78 expect(t, c, "") 79 c.Add(&s, data("a", sa, avgCachedSize)) 80 expect(t, c, "a") 81 c.Add(&s, data("b", sb, avgCachedSize)) 82 expect(t, c, "b,a") 83 c.Add(&s, data("c", sc, avgCachedSize)) 84 expect(t, c, "c,b,a") 85 c.Add(&s, data("d", sd, avgCachedSize)) 86 expect(t, c, "d,c,b") 87 if _, ok := c.Find(&s, "a"); ok { 88 t.Errorf("a shouldn't be in the cache") 89 } 90 if res, ok := c.Find(&s, "c"); !ok { 91 t.Errorf("c should be in the cache") 92 } else if res.Memo != sc { 93 t.Errorf("invalid Memo for c") 94 } 95 expect(t, c, "c,d,b") 96 97 if res, ok := c.Find(&s, "b"); !ok { 98 t.Errorf("b should be in the cache") 99 } else if res.Memo != sb { 100 t.Errorf("invalid Memo for b") 101 } 102 expect(t, c, "b,c,d") 103 104 c.Add(&s, data("a", sa, avgCachedSize)) 105 expect(t, c, "a,b,c") 106 107 c.Purge("b") 108 expect(t, c, "a,c") 109 if _, ok := c.Find(&s, "b"); ok { 110 t.Errorf("b shouldn't be in the cache") 111 } 112 113 c.Purge("c") 114 expect(t, c, "a") 115 116 c.Add(&s, data("b", sb, avgCachedSize)) 117 expect(t, c, "b,a") 118 119 c.Clear() 120 expect(t, c, "") 121 if _, ok := c.Find(&s, "b"); ok { 122 t.Errorf("b shouldn't be in the cache") 123 } 124 } 125 126 func TestCacheMemory(t *testing.T) { 127 m := &memo.Memo{} 128 129 c := New(10 * avgCachedSize) 130 var s Session 131 s.Init() 132 expect(t, c, "") 133 for i := 0; i < 10; i++ { 134 c.Add(&s, data(fmt.Sprintf("%d", i), m, avgCachedSize/2)) 135 } 136 expect(t, c, "9,8,7,6,5,4,3,2,1,0") 137 138 // Verify handling when we have no more entries. 139 c.Add(&s, data("10", m, avgCachedSize/2)) 140 expect(t, c, "10,9,8,7,6,5,4,3,2,1") 141 142 // Verify handling when we have larger entries. 143 c.Add(&s, data("large", m, avgCachedSize*8)) 144 expect(t, c, "large,10,9,8,7") 145 c.Add(&s, data("verylarge", m, avgCachedSize*10)) 146 expect(t, c, "verylarge") 147 148 for i := 0; i < 10; i++ { 149 c.Add(&s, data(fmt.Sprintf("%d", i), m, avgCachedSize)) 150 } 151 expect(t, c, "9,8,7,6,5,4,3,2,1,0") 152 153 // Verify that we don't try to add an entry that's larger than the cache size. 154 c.Add(&s, data("large", m, avgCachedSize*11)) 155 expect(t, c, "9,8,7,6,5,4,3,2,1,0") 156 157 // Verify handling when we update an existing entry with one that uses more 158 // memory. 159 c.Add(&s, data("5", m, avgCachedSize*5)) 160 expect(t, c, "5,9,8,7,6,4") 161 162 c.Add(&s, data("0", m, avgCachedSize)) 163 expect(t, c, "0,5,9,8,7,6") 164 165 // Verify handling when we update an existing entry with one that uses less 166 // memory. 167 c.Add(&s, data("5", m, avgCachedSize)) 168 expect(t, c, "5,0,9,8,7,6") 169 c.Add(&s, data("1", m, avgCachedSize)) 170 c.Add(&s, data("2", m, avgCachedSize)) 171 c.Add(&s, data("3", m, avgCachedSize)) 172 c.Add(&s, data("4", m, avgCachedSize)) 173 expect(t, c, "4,3,2,1,5,0,9,8,7,6") 174 175 // Verify Purge updates the available memory. 176 c.Purge("3") 177 expect(t, c, "4,2,1,5,0,9,8,7,6") 178 c.Add(&s, data("x", m, avgCachedSize)) 179 expect(t, c, "x,4,2,1,5,0,9,8,7,6") 180 c.Add(&s, data("y", m, avgCachedSize)) 181 expect(t, c, "y,x,4,2,1,5,0,9,8,7") 182 } 183 184 // TestSynchronization verifies that the cache doesn't crash (or cause a race 185 // detector error) when multiple goroutines are using it in parallel. 186 func TestSynchronization(t *testing.T) { 187 const size = 100 188 c := New(size * avgCachedSize) 189 190 var wg sync.WaitGroup 191 const goroutines = 20 192 wg.Add(goroutines) 193 for i := 0; i < goroutines; i++ { 194 go func() { 195 rng, _ := randutil.NewPseudoRand() 196 var s Session 197 s.Init() 198 for j := 0; j < 5000; j++ { 199 sql := fmt.Sprintf("%d", rng.Intn(2*size)) 200 switch r := rng.Intn(100); { 201 case r == 0: 202 // 1% of the time, clear the entire cache. 203 c.Clear() 204 case r <= 10: 205 // 10% of the time, purge an entry. 206 c.Purge(sql) 207 case r <= 35: 208 // 25% of the time, add an entry. 209 c.Add(&s, data(sql, &memo.Memo{}, int64(256+rng.Intn(10*avgCachedSize)))) 210 default: 211 // The rest of the time, find an entry. 212 _, _ = c.Find(&s, sql) 213 } 214 c.check() 215 } 216 wg.Done() 217 }() 218 } 219 wg.Wait() 220 } 221 222 func TestSession(t *testing.T) { 223 var s Session 224 s.Init() 225 226 // Find the number of misses that are necessary to reach the high miss ratio. 227 n := 1 228 for { 229 n++ 230 s.registerMiss() 231 if s.highMissRatio() { 232 break 233 } 234 } 235 if n < 1000 { 236 t.Errorf("high miss ratio threshold reached too quickly (n=%d)", n) 237 } else if n > 10000 { 238 t.Errorf("high miss ratio threshold reached too slowly (n=%d)", n) 239 } 240 241 // Verify we can get the average close to 0.5. 242 for i := 0; i < 2000; i++ { 243 s.registerHit() 244 s.registerMiss() 245 } 246 v := float64(s.missRatioMMA) / mmaScale 247 if v < 0.5 || v > 0.6 { 248 t.Errorf("invalid miss ratio %f, expected close to 0.5", v) 249 } 250 // Verify we can get the average close to 0. 251 for i := 0; i < 2000; i++ { 252 s.registerHit() 253 } 254 v = float64(s.missRatioMMA) / mmaScale 255 if v > 0.1 { 256 t.Errorf("invalid miss ratio %f, expected close to 0", v) 257 } 258 } 259 260 // BenchmarkWorstCase is a worst case benchmark where every session 261 // misses the cache. The result of the benchmark is the time to do a pair of 262 // Find, Add operations. 263 // 264 // For server-level benchmarks, see BenchmarkQueryCache in pkg/sql. 265 func BenchmarkWorstCase(b *testing.B) { 266 for _, mitigation := range []bool{false, true} { 267 name := "NoMitigation" 268 if mitigation { 269 name = "WithMitigation" 270 } 271 b.Run(name, func(b *testing.B) { 272 for _, numWorkers := range []int{1, 10, 100} { 273 b.Run(fmt.Sprintf("%d", numWorkers), func(b *testing.B) { 274 const cacheSize = 10 275 c := New(cacheSize * maxCachedSize) 276 numOpsPerWorker := (b.N + numWorkers - 1) / numWorkers 277 var wg sync.WaitGroup 278 wg.Add(numWorkers) 279 for i := 0; i < numWorkers; i++ { 280 workerID := i 281 go func() { 282 var s Session 283 s.Init() 284 cd := CachedData{Memo: &memo.Memo{}} 285 for j := 0; j < numOpsPerWorker; j++ { 286 str := fmt.Sprintf("%d/%d", workerID, j) 287 if _, ok := c.Find(&s, str); ok { 288 panic("found") 289 } 290 cd.SQL = str 291 if !mitigation { 292 // Disable the mitigation mechanism. 293 s.missRatioMMA = 0 294 } 295 c.Add(&s, &cd) 296 } 297 wg.Done() 298 }() 299 } 300 wg.Wait() 301 }) 302 } 303 }) 304 } 305 }