github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/objstorage/objstorageprovider/sharedcache/shared_cache_test.go (about) 1 package sharedcache_test 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strconv" 8 "sync" 9 "testing" 10 "time" 11 12 "github.com/cockroachdb/datadriven" 13 "github.com/cockroachdb/pebble/internal/base" 14 "github.com/cockroachdb/pebble/internal/invariants" 15 "github.com/cockroachdb/pebble/objstorage" 16 "github.com/cockroachdb/pebble/objstorage/objstorageprovider" 17 "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache" 18 "github.com/cockroachdb/pebble/vfs" 19 "github.com/stretchr/testify/require" 20 "golang.org/x/exp/rand" 21 ) 22 23 func TestSharedCache(t *testing.T) { 24 ctx := context.Background() 25 26 datadriven.Walk(t, "testdata/cache", func(t *testing.T, path string) { 27 var log base.InMemLogger 28 fs := vfs.WithLogging(vfs.NewMem(), func(fmt string, args ...interface{}) { 29 log.Infof("<local fs> "+fmt, args...) 30 }) 31 32 provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(fs, "")) 33 require.NoError(t, err) 34 35 var cache *sharedcache.Cache 36 defer func() { 37 if cache != nil { 38 cache.Close() 39 } 40 }() 41 42 var objData []byte 43 datadriven.RunTest(t, path, func(t *testing.T, d *datadriven.TestData) string { 44 log.Reset() 45 switch d.Cmd { 46 case "init": 47 blockSize := parseBytesArg(t, d, "block-size", 32*1024) 48 shardingBlockSize := parseBytesArg(t, d, "sharding-block-size", 1024*1024) 49 numShards := parseBytesArg(t, d, "num-shards", 32) 50 size := parseBytesArg(t, d, "size", numShards*shardingBlockSize) 51 if size%(numShards*shardingBlockSize) != 0 { 52 d.Fatalf(t, "size (%d) must be a multiple of numShards (%d) * shardingBlockSize(%d)", 53 size, numShards, shardingBlockSize, 54 ) 55 } 56 cache, err = sharedcache.Open( 57 fs, base.DefaultLogger, "", blockSize, int64(shardingBlockSize), int64(size), numShards, 58 ) 59 require.NoError(t, err) 60 return fmt.Sprintf("initialized with block-size=%d size=%d num-shards=%d", blockSize, size, numShards) 61 62 case "write": 63 size := mustParseBytesArg(t, d, "size") 64 65 writable, _, err := provider.Create(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.CreateOptions{}) 66 require.NoError(t, err) 67 defer writable.Finish() 68 69 // With invariants on, Write will modify its input buffer. 70 objData = make([]byte, size) 71 wrote := make([]byte, size) 72 for i := 0; i < size; i++ { 73 objData[i] = byte(i) 74 wrote[i] = byte(i) 75 } 76 err = writable.Write(wrote) 77 // Writing a file is test setup, and it always is expected to succeed, so we assert 78 // within the test, rather than returning n and/or err here. 79 require.NoError(t, err) 80 81 return "" 82 case "read", "read-for-compaction": 83 missesBefore := cache.Metrics().ReadsWithPartialHit + cache.Metrics().ReadsWithNoHit 84 offset := mustParseBytesArg(t, d, "offset") 85 size := mustParseBytesArg(t, d, "size") 86 87 readable, err := provider.OpenForReading(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.OpenOptions{}) 88 require.NoError(t, err) 89 defer readable.Close() 90 91 got := make([]byte, size) 92 flags := sharedcache.ReadFlags{ 93 ReadOnly: d.Cmd == "read-for-compaction", 94 } 95 err = cache.ReadAt(ctx, base.FileNum(1).DiskFileNum(), got, int64(offset), readable, readable.Size(), flags) 96 // We always expect cache.ReadAt to succeed. 97 require.NoError(t, err) 98 // It is easier to assert this condition programmatically, rather than returning 99 // got, which may be very large. 100 require.True(t, bytes.Equal(objData[int(offset):int(offset)+size], got), "incorrect data returned") 101 102 // In order to ensure we get a hit on the next read, we must wait for writing to 103 // the cache to complete. 104 cache.WaitForWritesToComplete() 105 106 // TODO(josh): Not tracing out filesystem activity here, since logging_fs.go 107 // doesn't trace calls to ReadAt or WriteAt. We should consider changing this. 108 missesAfter := cache.Metrics().ReadsWithPartialHit + cache.Metrics().ReadsWithNoHit 109 return fmt.Sprintf("misses=%d", missesAfter-missesBefore) 110 default: 111 d.Fatalf(t, "unknown command %s", d.Cmd) 112 return "" 113 } 114 }) 115 }) 116 } 117 118 func TestSharedCacheRandomized(t *testing.T) { 119 ctx := context.Background() 120 121 var log base.InMemLogger 122 fs := vfs.WithLogging(vfs.NewMem(), func(fmt string, args ...interface{}) { 123 log.Infof("<local fs> "+fmt, args...) 124 }) 125 126 provider, err := objstorageprovider.Open(objstorageprovider.DefaultSettings(fs, "")) 127 require.NoError(t, err) 128 129 seed := uint64(time.Now().UnixNano()) 130 fmt.Printf("seed: %v\n", seed) 131 rand.Seed(seed) 132 133 helper := func( 134 blockSize int, 135 shardingBlockSize int64) func(t *testing.T) { 136 return func(t *testing.T) { 137 for _, concurrentReads := range []bool{false, true} { 138 t.Run(fmt.Sprintf("concurrentReads=%v", concurrentReads), func(t *testing.T) { 139 maxShards := 32 140 if invariants.RaceEnabled { 141 maxShards = 8 142 } 143 numShards := rand.Intn(maxShards) + 1 144 cacheSize := shardingBlockSize * int64(numShards) // minimum allowed cache size 145 146 cache, err := sharedcache.Open(fs, base.DefaultLogger, "", blockSize, shardingBlockSize, cacheSize, numShards) 147 require.NoError(t, err) 148 defer cache.Close() 149 150 writable, _, err := provider.Create(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.CreateOptions{}) 151 require.NoError(t, err) 152 153 // With invariants on, Write will modify its input buffer. 154 // If size == 0, we can see panics below, so force a nonzero size. 155 size := rand.Int63n(cacheSize-1) + 1 156 objData := make([]byte, size) 157 wrote := make([]byte, size) 158 for i := 0; i < int(size); i++ { 159 objData[i] = byte(i) 160 wrote[i] = byte(i) 161 } 162 163 require.NoError(t, writable.Write(wrote)) 164 require.NoError(t, writable.Finish()) 165 166 readable, err := provider.OpenForReading(ctx, base.FileTypeTable, base.FileNum(1).DiskFileNum(), objstorage.OpenOptions{}) 167 require.NoError(t, err) 168 defer readable.Close() 169 170 const numDistinctReads = 100 171 wg := sync.WaitGroup{} 172 for i := 0; i < numDistinctReads; i++ { 173 wg.Add(1) 174 go func() { 175 defer wg.Done() 176 offset := rand.Int63n(size) 177 178 got := make([]byte, size-offset) 179 err := cache.ReadAt(ctx, base.FileNum(1).DiskFileNum(), got, offset, readable, readable.Size(), sharedcache.ReadFlags{}) 180 require.NoError(t, err) 181 require.Equal(t, objData[int(offset):], got) 182 183 got = make([]byte, size-offset) 184 err = cache.ReadAt(ctx, base.FileNum(1).DiskFileNum(), got, offset, readable, readable.Size(), sharedcache.ReadFlags{}) 185 require.NoError(t, err) 186 require.Equal(t, objData[int(offset):], got) 187 }() 188 // If concurrent reads, only wait 50% of loop iterations on average. 189 if concurrentReads && rand.Intn(2) == 0 { 190 wg.Wait() 191 } 192 if !concurrentReads { 193 wg.Wait() 194 } 195 } 196 wg.Wait() 197 }) 198 } 199 } 200 } 201 t.Run("32 KB block size", helper(32*1024, 1024*1024)) 202 t.Run("1 MB block size", helper(1024*1024, 1024*1024)) 203 204 if !invariants.RaceEnabled { 205 for i := 0; i < 5; i++ { 206 exp := rand.Intn(11) + 10 // [10, 20] 207 randomBlockSize := 1 << exp // [1 KB, 1 MB] 208 209 factor := rand.Intn(4) + 1 // [1, 4] 210 randomShardingBlockSize := int64(randomBlockSize * factor) // [1 KB, 4 MB] 211 212 t.Run("random block and sharding block size", helper(randomBlockSize, randomShardingBlockSize)) 213 } 214 } 215 } 216 217 // parseBytesArg parses an optional argument that specifies a byte size; if the 218 // argument is not specified the default value is used. K/M/G suffixes are 219 // supported. 220 func parseBytesArg(t testing.TB, d *datadriven.TestData, argName string, defaultValue int) int { 221 res, ok := tryParseBytesArg(t, d, argName) 222 if !ok { 223 return defaultValue 224 } 225 return res 226 } 227 228 // parseBytesArg parses a mandatory argument that specifies a byte size; K/M/G 229 // suffixes are supported. 230 func mustParseBytesArg(t testing.TB, d *datadriven.TestData, argName string) int { 231 res, ok := tryParseBytesArg(t, d, argName) 232 if !ok { 233 t.Fatalf("argument '%s' missing", argName) 234 } 235 return res 236 } 237 238 func tryParseBytesArg(t testing.TB, d *datadriven.TestData, argName string) (val int, ok bool) { 239 arg, ok := d.Arg(argName) 240 if !ok { 241 return 0, false 242 } 243 if len(arg.Vals) != 1 { 244 t.Fatalf("expected 1 value for '%s'", argName) 245 } 246 v := arg.Vals[0] 247 factor := 1 248 switch v[len(v)-1] { 249 case 'k', 'K': 250 factor = 1024 251 case 'm', 'M': 252 factor = 1024 * 1024 253 case 'g', 'G': 254 factor = 1024 * 1024 * 1024 255 } 256 if factor > 1 { 257 v = v[:len(v)-1] 258 } 259 res, err := strconv.Atoi(v) 260 if err != nil { 261 t.Fatalf("could not parse value '%s' for '%s'", arg.Vals[0], argName) 262 } 263 264 return res * factor, true 265 }