github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/commit_test.go (about) 1 // Copyright 2018 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package pebble 6 7 import ( 8 "encoding/binary" 9 "fmt" 10 "io" 11 "sync" 12 "sync/atomic" 13 "testing" 14 "time" 15 16 "github.com/cockroachdb/pebble/internal/arenaskl" 17 "github.com/cockroachdb/pebble/internal/invariants" 18 "github.com/cockroachdb/pebble/record" 19 "github.com/cockroachdb/pebble/vfs" 20 "github.com/prometheus/client_golang/prometheus" 21 "github.com/stretchr/testify/require" 22 "golang.org/x/exp/rand" 23 ) 24 25 type testCommitEnv struct { 26 logSeqNum atomic.Uint64 27 visibleSeqNum atomic.Uint64 28 writeCount atomic.Uint64 29 applyBuf struct { 30 sync.Mutex 31 buf []uint64 32 } 33 queueSemChan chan struct{} 34 } 35 36 func (e *testCommitEnv) env() commitEnv { 37 return commitEnv{ 38 logSeqNum: &e.logSeqNum, 39 visibleSeqNum: &e.visibleSeqNum, 40 apply: e.apply, 41 write: e.write, 42 } 43 } 44 45 func (e *testCommitEnv) apply(b *Batch, mem *memTable) error { 46 e.applyBuf.Lock() 47 e.applyBuf.buf = append(e.applyBuf.buf, b.SeqNum()) 48 e.applyBuf.Unlock() 49 return nil 50 } 51 52 func (e *testCommitEnv) write(b *Batch, wg *sync.WaitGroup, _ *error) (*memTable, error) { 53 e.writeCount.Add(1) 54 if wg != nil { 55 wg.Done() 56 <-e.queueSemChan 57 } 58 return nil, nil 59 } 60 61 func TestCommitQueue(t *testing.T) { 62 var q commitQueue 63 var batches [16]Batch 64 for i := range batches { 65 q.enqueue(&batches[i]) 66 } 67 if b := q.dequeueApplied(); b != nil { 68 t.Fatalf("unexpectedly dequeued batch: %p", b) 69 } 70 batches[1].applied.Store(true) 71 if b := q.dequeueApplied(); b != nil { 72 t.Fatalf("unexpectedly dequeued batch: %p", b) 73 } 74 for i := range batches { 75 batches[i].applied.Store(true) 76 if b := q.dequeueApplied(); b != &batches[i] { 77 t.Fatalf("%d: expected batch %p, but found %p", i, &batches[i], b) 78 } 79 } 80 if b := q.dequeueApplied(); b != nil { 81 t.Fatalf("unexpectedly dequeued batch: %p", b) 82 } 83 } 84 85 func TestCommitPipeline(t *testing.T) { 86 var e testCommitEnv 87 p := newCommitPipeline(e.env()) 88 89 n := 10000 90 if invariants.RaceEnabled { 91 // Under race builds we have to limit the concurrency or we hit the 92 // following error: 93 // 94 // race: limit on 8128 simultaneously alive goroutines is exceeded, dying 95 n = 1000 96 } 97 98 var wg sync.WaitGroup 99 wg.Add(n) 100 for i := 0; i < n; i++ { 101 go func(i int) { 102 defer wg.Done() 103 var b Batch 104 _ = b.Set([]byte(fmt.Sprint(i)), nil, nil) 105 _ = p.Commit(&b, false, false) 106 }(i) 107 } 108 wg.Wait() 109 110 if s := e.writeCount.Load(); uint64(n) != s { 111 t.Fatalf("expected %d written batches, but found %d", n, s) 112 } 113 if n != len(e.applyBuf.buf) { 114 t.Fatalf("expected %d written batches, but found %d", 115 n, len(e.applyBuf.buf)) 116 } 117 if s := e.logSeqNum.Load(); uint64(n) != s { 118 t.Fatalf("expected %d, but found %d", n, s) 119 } 120 if s := e.visibleSeqNum.Load(); uint64(n) != s { 121 t.Fatalf("expected %d, but found %d", n, s) 122 } 123 } 124 125 func TestCommitPipelineSync(t *testing.T) { 126 n := 10000 127 if invariants.RaceEnabled { 128 // Under race builds we have to limit the concurrency or we hit the 129 // following error: 130 // 131 // race: limit on 8128 simultaneously alive goroutines is exceeded, dying 132 n = 1000 133 } 134 135 for _, noSyncWait := range []bool{false, true} { 136 t.Run(fmt.Sprintf("no-sync-wait=%t", noSyncWait), func(t *testing.T) { 137 var e testCommitEnv 138 p := newCommitPipeline(e.env()) 139 e.queueSemChan = p.logSyncQSem 140 141 var wg sync.WaitGroup 142 wg.Add(n) 143 for i := 0; i < n; i++ { 144 go func(i int) { 145 defer wg.Done() 146 var b Batch 147 require.NoError(t, b.Set([]byte(fmt.Sprint(i)), nil, nil)) 148 require.NoError(t, p.Commit(&b, true, noSyncWait)) 149 if noSyncWait { 150 require.NoError(t, b.SyncWait()) 151 } 152 }(i) 153 } 154 wg.Wait() 155 if s := e.writeCount.Load(); uint64(n) != s { 156 t.Fatalf("expected %d written batches, but found %d", n, s) 157 } 158 if n != len(e.applyBuf.buf) { 159 t.Fatalf("expected %d written batches, but found %d", 160 n, len(e.applyBuf.buf)) 161 } 162 if s := e.logSeqNum.Load(); uint64(n) != s { 163 t.Fatalf("expected %d, but found %d", n, s) 164 } 165 if s := e.visibleSeqNum.Load(); uint64(n) != s { 166 t.Fatalf("expected %d, but found %d", n, s) 167 } 168 }) 169 } 170 } 171 172 func TestCommitPipelineAllocateSeqNum(t *testing.T) { 173 var e testCommitEnv 174 p := newCommitPipeline(e.env()) 175 176 const n = 10 177 var wg sync.WaitGroup 178 wg.Add(n) 179 var prepareCount atomic.Uint64 180 var applyCount atomic.Uint64 181 for i := 1; i <= n; i++ { 182 go func(i int) { 183 defer wg.Done() 184 p.AllocateSeqNum(i, func(_ uint64) { 185 prepareCount.Add(1) 186 }, func(seqNum uint64) { 187 applyCount.Add(1) 188 }) 189 }(i) 190 } 191 wg.Wait() 192 193 if s := prepareCount.Load(); n != s { 194 t.Fatalf("expected %d prepares, but found %d", n, s) 195 } 196 if s := applyCount.Load(); n != s { 197 t.Fatalf("expected %d applies, but found %d", n, s) 198 } 199 // AllocateSeqNum always returns a non-zero sequence number causing the 200 // values we see to be offset from 1. 201 const total = 1 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 202 if s := e.logSeqNum.Load(); total != s { 203 t.Fatalf("expected %d, but found %d", total, s) 204 } 205 if s := e.visibleSeqNum.Load(); total != s { 206 t.Fatalf("expected %d, but found %d", total, s) 207 } 208 } 209 210 type syncDelayFile struct { 211 vfs.File 212 done chan struct{} 213 } 214 215 func (f *syncDelayFile) Sync() error { 216 <-f.done 217 return nil 218 } 219 220 func TestCommitPipelineWALClose(t *testing.T) { 221 // This test stresses the edge case of N goroutines blocked in the 222 // commitPipeline waiting for the log to sync when we concurrently decide to 223 // rotate and close the log. 224 225 mem := vfs.NewMem() 226 f, err := mem.Create("test-wal") 227 require.NoError(t, err) 228 229 // syncDelayFile will block on the done channel befor returning from Sync 230 // call. 231 sf := &syncDelayFile{ 232 File: f, 233 done: make(chan struct{}), 234 } 235 236 // A basic commitEnv which writes to a WAL. 237 var wal *record.LogWriter 238 var walDone sync.WaitGroup 239 testEnv := commitEnv{ 240 logSeqNum: new(atomic.Uint64), 241 visibleSeqNum: new(atomic.Uint64), 242 apply: func(b *Batch, mem *memTable) error { 243 // At this point, we've called SyncRecord but the sync is blocked. 244 walDone.Done() 245 return nil 246 }, 247 write: func(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*memTable, error) { 248 _, err := wal.SyncRecord(b.data, syncWG, syncErr) 249 return nil, err 250 }, 251 } 252 p := newCommitPipeline(testEnv) 253 wal = record.NewLogWriter(sf, 0 /* logNum */, record.LogWriterConfig{ 254 WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), 255 QueueSemChan: p.logSyncQSem, 256 }) 257 258 // Launch N (commitConcurrency) goroutines which each create a batch and 259 // commit it with sync==true. Because of the syncDelayFile, none of these 260 // operations can complete until syncDelayFile.done is closed. 261 errCh := make(chan error, cap(p.commitQueueSem)) 262 walDone.Add(cap(errCh)) 263 for i := 0; i < cap(errCh); i++ { 264 go func(i int) { 265 b := &Batch{} 266 if err := b.LogData([]byte("foo"), nil); err != nil { 267 errCh <- err 268 return 269 } 270 errCh <- p.Commit(b, true /* sync */, false) 271 }(i) 272 } 273 274 // Wait for all of the WAL writes to queue up. This ensures we don't violate 275 // the concurrency requirements of LogWriter, and also ensures all of the WAL 276 // writes are queued. 277 walDone.Wait() 278 close(sf.done) 279 280 // Close the WAL. A "queue is full" panic means that something is broken. 281 require.NoError(t, wal.Close()) 282 for i := 0; i < cap(errCh); i++ { 283 require.NoError(t, <-errCh) 284 } 285 } 286 287 func BenchmarkCommitPipeline(b *testing.B) { 288 for _, noSyncWait := range []bool{false, true} { 289 for _, parallelism := range []int{1, 2, 4, 8, 16, 32, 64, 128} { 290 b.Run(fmt.Sprintf("no-sync-wait=%t/parallel=%d", noSyncWait, parallelism), 291 func(b *testing.B) { 292 b.SetParallelism(parallelism) 293 mem := newMemTable(memTableOptions{}) 294 var wal *record.LogWriter 295 nullCommitEnv := commitEnv{ 296 logSeqNum: new(atomic.Uint64), 297 visibleSeqNum: new(atomic.Uint64), 298 apply: func(b *Batch, mem *memTable) error { 299 err := mem.apply(b, b.SeqNum()) 300 if err != nil { 301 return err 302 } 303 mem.writerUnref() 304 return nil 305 }, 306 write: func(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*memTable, error) { 307 for { 308 err := mem.prepare(b) 309 if err == arenaskl.ErrArenaFull { 310 mem = newMemTable(memTableOptions{}) 311 continue 312 } 313 if err != nil { 314 return nil, err 315 } 316 break 317 } 318 319 _, err := wal.SyncRecord(b.data, syncWG, syncErr) 320 return mem, err 321 }, 322 } 323 p := newCommitPipeline(nullCommitEnv) 324 wal = record.NewLogWriter(io.Discard, 0, /* logNum */ 325 record.LogWriterConfig{ 326 WALFsyncLatency: prometheus.NewHistogram(prometheus.HistogramOpts{}), 327 QueueSemChan: p.logSyncQSem, 328 }) 329 const keySize = 8 330 b.SetBytes(2 * keySize) 331 b.ResetTimer() 332 333 b.RunParallel(func(pb *testing.PB) { 334 rng := rand.New(rand.NewSource(uint64(time.Now().UnixNano()))) 335 buf := make([]byte, keySize) 336 337 for pb.Next() { 338 batch := newBatch(nil) 339 binary.BigEndian.PutUint64(buf, rng.Uint64()) 340 batch.Set(buf, buf, nil) 341 if err := p.Commit(batch, true /* sync */, noSyncWait); err != nil { 342 b.Fatal(err) 343 } 344 if noSyncWait { 345 if err := batch.SyncWait(); err != nil { 346 b.Fatal(err) 347 } 348 } 349 batch.release() 350 } 351 }) 352 }) 353 } 354 } 355 }