github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/disk_block_cache_test.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libkbfs 6 7 import ( 8 "math" 9 "math/rand" 10 "os" 11 "sync" 12 "testing" 13 "time" 14 15 "github.com/keybase/client/go/kbfs/data" 16 "github.com/keybase/client/go/kbfs/env" 17 "github.com/keybase/client/go/kbfs/ioutil" 18 "github.com/keybase/client/go/kbfs/kbfsblock" 19 "github.com/keybase/client/go/kbfs/kbfscrypto" 20 "github.com/keybase/client/go/kbfs/kbfsmd" 21 "github.com/keybase/client/go/kbfs/test/clocktest" 22 "github.com/keybase/client/go/kbfs/tlf" 23 "github.com/keybase/client/go/protocol/keybase1" 24 "github.com/stretchr/testify/require" 25 "github.com/syndtr/goleveldb/leveldb/errors" 26 "github.com/syndtr/goleveldb/leveldb/util" 27 "golang.org/x/net/context" 28 ) 29 30 const ( 31 testDiskBlockCacheMaxBytes int64 = 1 << 21 32 ) 33 34 type testDiskBlockCacheConfig struct { 35 codecGetter 36 logMaker 37 *testClockGetter 38 limiter DiskLimiter 39 syncedTlfGetterSetter 40 initModeGetter 41 bcache data.BlockCache 42 } 43 44 func newTestDiskBlockCacheConfig(t *testing.T) *testDiskBlockCacheConfig { 45 return &testDiskBlockCacheConfig{ 46 newTestCodecGetter(), 47 newTestLogMaker(t), 48 newTestClockGetter(), 49 nil, 50 newTestSyncedTlfGetterSetter(), 51 testInitModeGetter{InitDefault}, 52 data.NewBlockCacheStandard(100, 100), 53 } 54 } 55 56 func (c testDiskBlockCacheConfig) DiskLimiter() DiskLimiter { 57 return c.limiter 58 } 59 60 func (c testDiskBlockCacheConfig) BlockCache() data.BlockCache { 61 return c.bcache 62 } 63 64 func newDiskBlockCacheForTest(config *testDiskBlockCacheConfig, 65 maxBytes int64) (*diskBlockCacheWrapped, error) { 66 maxFiles := int64(10000) 67 workingSetCache, err := newDiskBlockCacheLocalForTest(config, 68 workingSetCacheLimitTrackerType) 69 if err != nil { 70 return nil, err 71 } 72 syncCache, err := newDiskBlockCacheLocalForTest( 73 config, syncCacheLimitTrackerType) 74 if err != nil { 75 return nil, err 76 } 77 err = workingSetCache.WaitUntilStarted() 78 if err != nil { 79 return nil, err 80 } 81 err = syncCache.WaitUntilStarted() 82 if err != nil { 83 return nil, err 84 } 85 params := backpressureDiskLimiterParams{ 86 minThreshold: 0.5, 87 maxThreshold: 0.95, 88 quotaMinThreshold: 1.0, 89 quotaMaxThreshold: 1.2, 90 journalFrac: 0.25, 91 diskCacheFrac: 0.25, 92 syncCacheFrac: 0.25, 93 byteLimit: testDiskBlockCacheMaxBytes, 94 fileLimit: maxFiles, 95 maxDelay: time.Second, 96 delayFn: defaultDoDelay, 97 freeBytesAndFilesFn: func() (int64, int64, error) { 98 // hackity hackeroni: simulate the disk cache taking up space. 99 syncBytes, workingBytes := testGetDiskCacheBytes( 100 syncCache, workingSetCache) 101 freeBytes := maxBytes - syncBytes - workingBytes 102 return freeBytes, maxFiles, nil 103 }, 104 quotaFn: func( 105 context.Context, keybase1.UserOrTeamID) (int64, int64) { 106 return 0, math.MaxInt64 107 }, 108 } 109 config.limiter, err = newBackpressureDiskLimiter( 110 config.MakeLogger(""), params) 111 if err != nil { 112 return nil, err 113 } 114 return &diskBlockCacheWrapped{ 115 config: config, 116 storageRoot: "", 117 workingSetCache: workingSetCache, 118 syncCache: syncCache, 119 }, nil 120 } 121 122 func initDiskBlockCacheTest(t *testing.T) (*diskBlockCacheWrapped, 123 *testDiskBlockCacheConfig) { 124 config := newTestDiskBlockCacheConfig(t) 125 cache, err := newDiskBlockCacheForTest(config, 126 testDiskBlockCacheMaxBytes) 127 require.NoError(t, err) 128 return cache, config 129 } 130 131 type testDiskBlockCacheGetter struct { 132 lock sync.RWMutex 133 cache DiskBlockCache 134 } 135 136 func (dbcg *testDiskBlockCacheGetter) DiskBlockCache() DiskBlockCache { 137 dbcg.lock.RLock() 138 defer dbcg.lock.RUnlock() 139 return dbcg.cache 140 } 141 142 func newTestDiskBlockCacheGetter(t *testing.T, 143 cache DiskBlockCache) *testDiskBlockCacheGetter { 144 return &testDiskBlockCacheGetter{cache: cache} 145 } 146 147 func shutdownDiskBlockCacheTest(cache DiskBlockCache) { 148 <-cache.Shutdown(context.Background()) 149 } 150 151 func setupRealBlockForDiskCache(t *testing.T, ptr data.BlockPointer, block data.Block, 152 config diskBlockCacheConfig) ([]byte, kbfscrypto.BlockCryptKeyServerHalf) { 153 blockEncoded, err := config.Codec().Encode(block) 154 require.NoError(t, err) 155 serverHalf, err := kbfscrypto.MakeRandomBlockCryptKeyServerHalf() 156 require.NoError(t, err) 157 return blockEncoded, serverHalf 158 } 159 160 func setupBlockForDiskCache(t *testing.T, config diskBlockCacheConfig) ( 161 data.BlockPointer, data.Block, []byte, kbfscrypto.BlockCryptKeyServerHalf) { 162 ptr := makeRandomBlockPointer(t) 163 block := makeFakeFileBlock(t, true) 164 blockEncoded, serverHalf := 165 setupRealBlockForDiskCache(t, ptr, block, config) 166 return ptr, block, blockEncoded, serverHalf 167 } 168 169 func TestDiskBlockCachePutAndGet(t *testing.T) { 170 t.Parallel() 171 t.Log("Test that basic disk cache Put and Get operations work.") 172 cache, config := initDiskBlockCacheTest(t) 173 defer shutdownDiskBlockCacheTest(cache) 174 175 tlf1 := tlf.FakeID(0, tlf.Private) 176 block1Ptr, _, block1Encoded, block1ServerHalf := setupBlockForDiskCache( 177 t, config) 178 179 ctx := context.Background() 180 181 t.Log("Put a block into the cache.") 182 err := cache.Put( 183 ctx, tlf1, block1Ptr.ID, block1Encoded, block1ServerHalf, 184 DiskBlockAnyCache) 185 require.NoError(t, err) 186 putMd, err := cache.GetMetadata(ctx, block1Ptr.ID) 187 require.NoError(t, err) 188 config.TestClock().Add(time.Second) 189 190 t.Log("Get that block from the cache. Verify that it's the same.") 191 buf, serverHalf, _, err := cache.Get( 192 ctx, tlf1, block1Ptr.ID, DiskBlockAnyCache) 193 require.NoError(t, err) 194 require.Equal(t, block1ServerHalf, serverHalf) 195 require.Equal(t, block1Encoded, buf) 196 197 t.Log("Verify that the Get updated the LRU time for the block.") 198 getMd, err := cache.GetMetadata(ctx, block1Ptr.ID) 199 require.NoError(t, err) 200 require.True(t, getMd.LRUTime.After(putMd.LRUTime.Time), "Get LRU time isn't "+ 201 "after the Put LRU time. Put metadata: %+v, Get metadata: %+v", 202 putMd, getMd) 203 204 t.Log("Attempt to Get a block from the cache that isn't there." + 205 " Verify that it fails.") 206 ptr2 := makeRandomBlockPointer(t) 207 buf, serverHalf, _, err = cache.Get( 208 ctx, tlf1, ptr2.ID, DiskBlockAnyCache) 209 require.EqualError(t, err, data.NoSuchBlockError{ID: ptr2.ID}.Error()) 210 require.Equal(t, kbfscrypto.BlockCryptKeyServerHalf{}, serverHalf) 211 require.Nil(t, buf) 212 213 t.Log("Verify that the cache returns no metadata for the missing block.") 214 _, err = cache.GetMetadata(ctx, ptr2.ID) 215 require.EqualError(t, err, errors.ErrNotFound.Error()) 216 } 217 218 func TestDiskBlockCacheDelete(t *testing.T) { 219 t.Parallel() 220 t.Log("Test that disk cache deletion works.") 221 cache, config := initDiskBlockCacheTest(t) 222 defer shutdownDiskBlockCacheTest(cache) 223 ctx := context.Background() 224 225 t.Log("Seed the cache with some other TLFs") 226 fakeTlfs := []byte{0, 1, 2, 4, 5} 227 for _, f := range fakeTlfs { 228 tlf := tlf.FakeID(f, tlf.Private) 229 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 230 t, config) 231 err := cache.Put( 232 ctx, tlf, blockPtr.ID, blockEncoded, serverHalf, DiskBlockAnyCache) 233 require.NoError(t, err) 234 } 235 tlf1 := tlf.FakeID(3, tlf.Private) 236 block1Ptr, _, block1Encoded, block1ServerHalf := setupBlockForDiskCache(t, 237 config) 238 block2Ptr, _, block2Encoded, block2ServerHalf := setupBlockForDiskCache(t, 239 config) 240 block3Ptr, _, block3Encoded, block3ServerHalf := setupBlockForDiskCache(t, 241 config) 242 243 t.Log("Put three blocks into the cache.") 244 err := cache.Put( 245 ctx, tlf1, block1Ptr.ID, block1Encoded, block1ServerHalf, 246 DiskBlockAnyCache) 247 require.NoError(t, err) 248 err = cache.Put( 249 ctx, tlf1, block2Ptr.ID, block2Encoded, block2ServerHalf, 250 DiskBlockAnyCache) 251 require.NoError(t, err) 252 err = cache.Put( 253 ctx, tlf1, block3Ptr.ID, block3Encoded, block3ServerHalf, 254 DiskBlockAnyCache) 255 require.NoError(t, err) 256 257 t.Log("Delete two of the blocks from the cache.") 258 _, _, err = cache.Delete( 259 ctx, []kbfsblock.ID{block1Ptr.ID, block2Ptr.ID}, DiskBlockAnyCache) 260 require.NoError(t, err) 261 262 t.Log("Verify that only the non-deleted block is still in the cache.") 263 _, _, _, err = cache.Get(ctx, tlf1, block1Ptr.ID, DiskBlockAnyCache) 264 require.EqualError(t, err, data.NoSuchBlockError{ID: block1Ptr.ID}.Error()) 265 _, _, _, err = cache.Get(ctx, tlf1, block2Ptr.ID, DiskBlockAnyCache) 266 require.EqualError(t, err, data.NoSuchBlockError{ID: block2Ptr.ID}.Error()) 267 _, _, _, err = cache.Get(ctx, tlf1, block3Ptr.ID, DiskBlockAnyCache) 268 require.NoError(t, err) 269 270 t.Log("Verify that the cache returns no LRU time for the missing blocks.") 271 _, err = cache.GetMetadata(ctx, block1Ptr.ID) 272 require.EqualError(t, err, errors.ErrNotFound.Error()) 273 _, err = cache.GetMetadata(ctx, block2Ptr.ID) 274 require.EqualError(t, err, errors.ErrNotFound.Error()) 275 } 276 277 func TestDiskBlockCacheEvictFromTLF(t *testing.T) { 278 t.Parallel() 279 t.Log("Test that disk cache eviction works for a single TLF.") 280 cache, config := initDiskBlockCacheTest(t) 281 standardCache := cache.workingSetCache 282 defer shutdownDiskBlockCacheTest(cache) 283 284 tlf1 := tlf.FakeID(3, tlf.Private) 285 ctx := context.Background() 286 clock := config.TestClock() 287 initialTime := clock.Now() 288 t.Log("Seed the cache with some other TLFs.") 289 fakeTlfs := []byte{0, 1, 2, 4, 5} 290 for _, f := range fakeTlfs { 291 tlf := tlf.FakeID(f, tlf.Private) 292 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 293 t, config) 294 err := standardCache.Put( 295 ctx, tlf, blockPtr.ID, blockEncoded, serverHalf) 296 require.NoError(t, err) 297 clock.Add(time.Second) 298 } 299 tlf1NumBlocks := 100 300 t.Log("Put 100 blocks into the cache.") 301 for i := 0; i < tlf1NumBlocks; i++ { 302 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 303 t, config) 304 err := standardCache.Put( 305 ctx, tlf1, blockPtr.ID, blockEncoded, serverHalf) 306 require.NoError(t, err) 307 clock.Add(time.Second) 308 } 309 310 previousAvgDuration := 50 * time.Second 311 averageDifference := float64(0) 312 numEvictionDifferences := 0 313 expectedCount := tlf1NumBlocks 314 315 t.Log("Incrementally evict all the tlf1 blocks in the cache.") 316 // Because the eviction algorithm is probabilistic, we can't rely on the 317 // same number of blocks being evicted every time. So we have to be smart 318 // about our assertions. 319 for expectedCount != 0 { 320 t.Log("Evict 10 blocks from the cache.") 321 numRemoved, _, err := standardCache.evictFromTLFLocked(ctx, tlf1, 10) 322 require.NoError(t, err) 323 expectedCount -= numRemoved 324 325 blockCount := 0 326 var avgDuration time.Duration 327 func() { 328 tlfBytes := tlf1.Bytes() 329 tlf1Range := util.BytesPrefix(tlfBytes) 330 iter := standardCache.tlfDb.NewIterator(tlf1Range, nil) 331 defer iter.Release() 332 for iter.Next() { 333 blockIDBytes := iter.Key()[len(tlfBytes):] 334 blockID, err := kbfsblock.IDFromBytes(blockIDBytes) 335 require.NoError(t, err) 336 putMd, err := standardCache.GetMetadata(ctx, blockID) 337 require.NoError(t, err) 338 avgDuration += putMd.LRUTime.Sub(initialTime) 339 blockCount++ 340 } 341 }() 342 t.Logf("Verify that there are %d blocks in the cache.", expectedCount) 343 require.Equal(t, expectedCount, blockCount, 344 "Removed %d blocks this round.", numRemoved) 345 if expectedCount > 0 { 346 avgDuration /= time.Duration(expectedCount) 347 t.Logf("Average LRU time of remaining blocks: %.2f", 348 avgDuration.Seconds()) 349 averageDifference += avgDuration.Seconds() - 350 previousAvgDuration.Seconds() 351 previousAvgDuration = avgDuration 352 numEvictionDifferences++ 353 } 354 } 355 t.Log("Verify that, on average, the LRU time of the blocks remaining in" + 356 " the queue keeps going up.") 357 averageDifference /= float64(numEvictionDifferences) 358 require.True(t, averageDifference > 3.0, 359 "Average overall LRU delta from an eviction: %.2f", averageDifference) 360 } 361 362 func TestDiskBlockCacheEvictOverall(t *testing.T) { 363 t.Parallel() 364 t.Log("Test that disk cache eviction works overall.") 365 cache, config := initDiskBlockCacheTest(t) 366 standardCache := cache.workingSetCache 367 defer shutdownDiskBlockCacheTest(cache) 368 369 ctx := context.Background() 370 clock := config.TestClock() 371 initialTime := clock.Now() 372 373 numTlfs := 10 374 numBlocksPerTlf := 10 375 totalBlocks := numTlfs * numBlocksPerTlf 376 377 t.Log("Seed the cache with some other TLFs.") 378 for i := byte(0); int(i) < numTlfs; i++ { 379 currTlf := tlf.FakeID(i, tlf.Private) 380 for j := 0; j < numBlocksPerTlf; j++ { 381 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 382 t, config) 383 err := standardCache.Put( 384 ctx, currTlf, blockPtr.ID, blockEncoded, serverHalf) 385 require.NoError(t, err) 386 clock.Add(time.Second) 387 } 388 } 389 390 // Average LRU will initially be half the total number of blocks, in 391 // seconds. 392 previousAvgDuration := time.Duration(totalBlocks>>1) * time.Second 393 averageDifference := float64(0) 394 numEvictionDifferences := 0 395 expectedCount := totalBlocks 396 397 t.Log("Incrementally evict all the blocks in the cache.") 398 // Because the eviction algorithm is probabilistic, we can't rely on the 399 // same number of blocks being evicted every time. So we have to be smart 400 // about our assertions. 401 for expectedCount != 0 { 402 t.Log("Evict 10 blocks from the cache.") 403 numRemoved, _, err := standardCache.evictLocked(ctx, 10) 404 require.NoError(t, err) 405 expectedCount -= numRemoved 406 407 blockCount := 0 408 var avgDuration time.Duration 409 func() { 410 iter := standardCache.metaDb.NewIterator(nil, nil) 411 defer iter.Release() 412 for iter.Next() { 413 metadata := DiskBlockCacheMetadata{} 414 err = config.Codec().Decode(iter.Value(), &metadata) 415 require.NoError(t, err) 416 avgDuration += metadata.LRUTime.Sub(initialTime) 417 blockCount++ 418 } 419 }() 420 t.Logf("Verify that there are %d blocks in the cache.", expectedCount) 421 require.Equal(t, expectedCount, blockCount, 422 "Removed %d blocks this round.", numRemoved) 423 if expectedCount > 0 { 424 avgDuration /= time.Duration(expectedCount) 425 t.Logf("Average LRU time of remaining blocks: %.2f", 426 avgDuration.Seconds()) 427 averageDifference += avgDuration.Seconds() - 428 previousAvgDuration.Seconds() 429 previousAvgDuration = avgDuration 430 numEvictionDifferences++ 431 } 432 } 433 t.Log("Verify that, on average, the LRU time of the blocks remaining in" + 434 " the queue keeps going up.") 435 averageDifference /= float64(numEvictionDifferences) 436 require.True(t, averageDifference > 3.0, 437 "Average overall LRU delta from an eviction: %.2f", averageDifference) 438 } 439 440 func TestDiskBlockCacheStaticLimit(t *testing.T) { 441 t.Parallel() 442 t.Log("Test that disk cache eviction works when we hit the static limit.") 443 cache, config := initDiskBlockCacheTest(t) 444 standardCache := cache.workingSetCache 445 defer shutdownDiskBlockCacheTest(cache) 446 447 ctx := context.Background() 448 clock := config.TestClock() 449 450 numTlfs := 10 451 numBlocksPerTlf := 5 452 numBlocks := numTlfs * numBlocksPerTlf 453 454 t.Log("Seed the cache with some blocks.") 455 for i := byte(0); int(i) < numTlfs; i++ { 456 currTlf := tlf.FakeID(i, tlf.Private) 457 for j := 0; j < numBlocksPerTlf; j++ { 458 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 459 t, config) 460 err := standardCache.Put( 461 ctx, currTlf, blockPtr.ID, blockEncoded, serverHalf) 462 require.NoError(t, err) 463 clock.Add(time.Second) 464 } 465 } 466 467 t.Log("Set the cache maximum bytes to the current total.") 468 currBytes := int64(standardCache.currBytes) 469 limiter := config.DiskLimiter().(*backpressureDiskLimiter) 470 limiter.diskCacheByteTracker.limit = currBytes 471 472 t.Log("Add a block to the cache. Verify that blocks were evicted.") 473 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 474 t, config) 475 err := standardCache.Put( 476 ctx, tlf.FakeID(10, tlf.Private), blockPtr.ID, blockEncoded, serverHalf) 477 require.NoError(t, err) 478 479 require.True(t, int64(standardCache.currBytes) < currBytes) 480 require.Equal( 481 t, 1+numBlocks-minNumBlocksToEvictInBatch, standardCache.numBlocks) 482 } 483 484 func TestDiskBlockCacheDynamicLimit(t *testing.T) { 485 t.Parallel() 486 t.Log("Test that disk cache eviction works when we hit a dynamic limit.") 487 cache, config := initDiskBlockCacheTest(t) 488 standardCache := cache.workingSetCache 489 defer shutdownDiskBlockCacheTest(cache) 490 491 ctx := context.Background() 492 clock := config.TestClock() 493 494 numTlfs := 10 495 numBlocksPerTlf := 5 496 numBlocks := numTlfs * numBlocksPerTlf 497 498 t.Log("Seed the cache with some blocks.") 499 for i := byte(0); int(i) < numTlfs; i++ { 500 currTlf := tlf.FakeID(i, tlf.Private) 501 for j := 0; j < numBlocksPerTlf; j++ { 502 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 503 t, config) 504 err := standardCache.Put( 505 ctx, currTlf, blockPtr.ID, blockEncoded, serverHalf) 506 require.NoError(t, err) 507 clock.Add(time.Second) 508 } 509 } 510 511 t.Log("Set the cache dynamic limit to its current value by tweaking the" + 512 " free space function.") 513 currBytes := int64(standardCache.currBytes) 514 limiter := config.DiskLimiter().(*backpressureDiskLimiter) 515 limiter.freeBytesAndFilesFn = func() (int64, int64, error) { 516 // Since the limit is 25% of the total available space, make that true 517 // for the current used byte count. We do this by setting the free 518 // byte count to 75% of the total, which is 3x used bytes. 519 freeBytes := currBytes * 3 520 // arbitrarily large number 521 numFiles := int64(100000000) 522 return freeBytes, numFiles, nil 523 } 524 525 t.Log("Add a round of blocks to the cache. Verify that blocks were" + 526 " evicted each time we went past the limit.") 527 start := numBlocks - minNumBlocksToEvictInBatch 528 for i := 1; i <= numBlocks; i++ { 529 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 530 t, config) 531 err := standardCache.Put( 532 ctx, tlf.FakeID(10, tlf.Private), blockPtr.ID, blockEncoded, 533 serverHalf) 534 require.NoError(t, err) 535 require.Equal( 536 t, start+(i%minNumBlocksToEvictInBatch), standardCache.numBlocks) 537 } 538 539 require.True(t, int64(standardCache.currBytes) < currBytes) 540 require.Equal(t, start, standardCache.numBlocks) 541 } 542 543 func TestDiskBlockCacheWithRetrievalQueue(t *testing.T) { 544 t.Parallel() 545 t.Log("Test the interaction of the disk block cache and retrieval queue.") 546 cache, dbcConfig := initDiskBlockCacheTest(t) 547 require.NotNil(t, cache) 548 defer shutdownDiskBlockCacheTest(cache) 549 550 t.Log("Create a queue with 0 workers to rule it out from serving blocks.") 551 bg := newFakeBlockGetter(false) 552 q := newBlockRetrievalQueue( 553 0, 0, 0, newTestBlockRetrievalConfig(t, bg, cache), 554 env.EmptyAppStateUpdater{}) 555 require.NotNil(t, q) 556 defer endBlockRetrievalQueueTest(t, q) 557 558 ctx := context.Background() 559 kmd := makeKMD() 560 ptr1, block1, block1Encoded, serverHalf1 := setupBlockForDiskCache( 561 t, dbcConfig) 562 err := cache.Put( 563 ctx, kmd.TlfID(), ptr1.ID, block1Encoded, serverHalf1, 564 DiskBlockAnyCache) 565 require.NoError(t, err) 566 // No workers initialized, so no need to clean up the continue ch since 567 // there will be nothing blocking on it. 568 _, _ = bg.setBlockToReturn(ptr1, block1) 569 570 t.Log("Request a block retrieval for ptr1. " + 571 "Verify the block against the one we put in the disk block cache.") 572 block := &data.FileBlock{} 573 ch := q.Request( 574 ctx, 1, kmd, ptr1, block, data.TransientEntry, BlockRequestWithPrefetch) 575 err = <-ch 576 require.NoError(t, err) 577 require.Equal(t, block1, block) 578 } 579 580 func seedDiskBlockCacheForTest(ctx context.Context, t *testing.T, 581 cache *diskBlockCacheWrapped, config diskBlockCacheConfig, numTlfs, 582 numBlocksPerTlf int) { 583 t.Log("Seed the cache with some blocks.") 584 clock := config.Clock().(*clocktest.TestClock) 585 for i := byte(0); int(i) < numTlfs; i++ { 586 currTlf := tlf.FakeID(i, tlf.Private) 587 for j := 0; j < numBlocksPerTlf; j++ { 588 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 589 t, config) 590 err := cache.Put( 591 ctx, currTlf, blockPtr.ID, blockEncoded, serverHalf, 592 DiskBlockSyncCache) 593 require.NoError(t, err) 594 clock.Add(time.Second) 595 } 596 } 597 } 598 599 func testPutBlockWhenSyncCacheFull( 600 ctx context.Context, t *testing.T, putCache *DiskBlockCacheLocal, 601 cache *diskBlockCacheWrapped, config *testDiskBlockCacheConfig) { 602 numTlfs := 10 603 numBlocksPerTlf := 5 604 numBlocks := numTlfs * numBlocksPerTlf 605 seedDiskBlockCacheForTest(ctx, t, cache, config, numTlfs, numBlocksPerTlf) 606 607 t.Log("Set the cache maximum bytes to the current total.") 608 require.Equal(t, 0, putCache.numBlocks) 609 currBytes := int64(cache.syncCache.currBytes) 610 limiter := config.DiskLimiter().(*backpressureDiskLimiter) 611 limiter.syncCacheByteTracker.limit = currBytes 612 613 t.Log("Add a block to the cache. Verify that no blocks were evicted " + 614 "and the working set got a new block.") 615 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 616 t, config) 617 err := putCache.Put( 618 ctx, tlf.FakeID(0, tlf.Private), blockPtr.ID, blockEncoded, serverHalf) 619 require.NoError(t, err) 620 621 require.Equal(t, int64(cache.syncCache.currBytes), currBytes) 622 require.Equal(t, numBlocks, cache.syncCache.numBlocks) 623 require.Equal(t, 1, putCache.numBlocks) 624 } 625 626 func TestSyncBlockCacheStaticLimit(t *testing.T) { 627 t.Parallel() 628 t.Log("Test that disk cache eviction doesn't happen in sync cache") 629 cache, config := initDiskBlockCacheTest(t) 630 defer shutdownDiskBlockCacheTest(cache) 631 ctx := context.Background() 632 633 testPutBlockWhenSyncCacheFull(ctx, t, cache.workingSetCache, cache, config) 634 } 635 636 func TestCrDirtyBlockCacheStaticLimit(t *testing.T) { 637 t.Parallel() 638 t.Log("Test that cr cache accepts blocks even when sync limit is hit") 639 cache, config := initDiskBlockCacheTest(t) 640 defer shutdownDiskBlockCacheTest(cache) 641 crCache, err := newDiskBlockCacheLocalForTest( 642 config, crDirtyBlockCacheLimitTrackerType) 643 require.NoError(t, err) 644 ctx := context.Background() 645 defer crCache.Shutdown(ctx) 646 647 err = crCache.WaitUntilStarted() 648 require.NoError(t, err) 649 650 testPutBlockWhenSyncCacheFull(ctx, t, crCache, cache, config) 651 } 652 653 func TestDiskBlockCacheLastUnrefPutAndGet(t *testing.T) { 654 t.Parallel() 655 t.Log("Test that basic disk cache last unref Put and Get operations work.") 656 cache, _ := initDiskBlockCacheTest(t) 657 defer shutdownDiskBlockCacheTest(cache) 658 659 ctx := context.Background() 660 661 t.Log("Put and get a last unref revision into the cache.") 662 tlf1 := tlf.FakeID(0, tlf.Private) 663 rev1 := kbfsmd.Revision(1) 664 ct := DiskBlockWorkingSetCache 665 err := cache.PutLastUnrefRev(ctx, tlf1, rev1, ct) 666 require.NoError(t, err) 667 getRev1, err := cache.GetLastUnrefRev(ctx, tlf1, ct) 668 require.NoError(t, err) 669 require.Equal(t, rev1, getRev1) 670 671 t.Log("Put and get a last unref revision into the cache for another TLF.") 672 tlf2 := tlf.FakeID(1, tlf.Public) 673 rev2 := kbfsmd.Revision(200) 674 err = cache.PutLastUnrefRev(ctx, tlf2, rev2, ct) 675 require.NoError(t, err) 676 getRev2, err := cache.GetLastUnrefRev(ctx, tlf2, ct) 677 require.NoError(t, err) 678 require.Equal(t, rev2, getRev2) 679 680 t.Log("Put a lower revision; should be ignored") 681 rev2b := kbfsmd.Revision(100) 682 err = cache.PutLastUnrefRev(ctx, tlf2, rev2b, ct) 683 require.NoError(t, err) 684 getRev2, err = cache.GetLastUnrefRev(ctx, tlf2, ct) 685 require.NoError(t, err) 686 require.Equal(t, rev2, getRev2) 687 688 // Force re-read from DB. 689 cache.syncCache.tlfLastUnrefs = nil 690 err = cache.syncCache.syncBlockCountsAndUnrefsFromDb() 691 require.NoError(t, err) 692 getRev1, err = cache.GetLastUnrefRev(ctx, tlf1, ct) 693 require.NoError(t, err) 694 require.Equal(t, rev1, getRev1) 695 getRev2, err = cache.GetLastUnrefRev(ctx, tlf2, ct) 696 require.NoError(t, err) 697 require.Equal(t, rev2, getRev2) 698 } 699 700 func TestDiskBlockCacheUnsyncTlf(t *testing.T) { 701 t.Parallel() 702 t.Log("Test that blocks are cleaned up after unsyncing a TLF.") 703 704 tempdir, err := ioutil.TempDir(os.TempDir(), "kbfscache") 705 require.NoError(t, err) 706 defer func() { 707 err := ioutil.RemoveAll(tempdir) 708 require.NoError(t, err) 709 }() 710 711 // Use a real config, since we need the real SetTlfSyncState 712 // implementation. 713 config, _, ctx, cancel := kbfsOpsInitNoMocks(t, "u1") 714 defer kbfsTestShutdownNoMocks(ctx, t, config, cancel) 715 716 clock := clocktest.NewTestClockNow() 717 config.SetClock(clock) 718 719 err = config.EnableDiskLimiter(tempdir) 720 require.NoError(t, err) 721 err = config.loadSyncedTlfsLocked() 722 require.NoError(t, err) 723 config.diskCacheMode = DiskCacheModeLocal 724 err = config.MakeDiskBlockCacheIfNotExists() 725 require.NoError(t, err) 726 cache := config.DiskBlockCache().(*diskBlockCacheWrapped) 727 defer func() { 728 <-cache.Shutdown(context.Background()) 729 }() 730 standardCache := cache.syncCache 731 err = standardCache.WaitUntilStarted() 732 require.NoError(t, err) 733 734 numTlfs := 3 735 numBlocksPerTlf := 5 736 numBlocks := numTlfs * numBlocksPerTlf 737 seedDiskBlockCacheForTest(ctx, t, cache, config, numTlfs, numBlocksPerTlf) 738 require.Equal(t, numBlocks, standardCache.numBlocks) 739 740 standardCache.clearTickerDuration = 0 741 standardCache.numBlocksToEvictOnClear = 1 742 743 tlfToUnsync := tlf.FakeID(1, tlf.Private) 744 ch, err := config.SetTlfSyncState(ctx, tlfToUnsync, FolderSyncConfig{ 745 Mode: keybase1.FolderSyncMode_DISABLED, 746 }) 747 require.NoError(t, err) 748 t.Log("Waiting for unsynced blocks to be cleared.") 749 err = <-ch 750 require.NoError(t, err) 751 require.Equal(t, numBlocks-numBlocksPerTlf, standardCache.numBlocks) 752 } 753 754 func TestDiskBlockCacheMoveBlock(t *testing.T) { 755 t.Parallel() 756 t.Log("Test that blocks can be moved between caches.") 757 cache, config := initDiskBlockCacheTest(t) 758 defer shutdownDiskBlockCacheTest(cache) 759 760 ctx := context.Background() 761 tlf1 := tlf.FakeID(0, tlf.Private) 762 block1Ptr, _, block1Encoded, block1ServerHalf := setupBlockForDiskCache( 763 t, config) 764 765 t.Log("Put a block into the default cache.") 766 err := cache.Put( 767 ctx, tlf1, block1Ptr.ID, block1Encoded, block1ServerHalf, 768 DiskBlockAnyCache) 769 require.NoError(t, err) 770 err = cache.UpdateMetadata( 771 ctx, tlf1, block1Ptr.ID, FinishedPrefetch, DiskBlockAnyCache) 772 require.NoError(t, err) 773 require.Equal(t, 1, cache.workingSetCache.numBlocks) 774 require.Equal(t, 0, cache.syncCache.numBlocks) 775 776 t.Log("Move the block by getting it with a different preferred cache.") 777 _, _, _, err = cache.Get(ctx, tlf1, block1Ptr.ID, DiskBlockSyncCache) 778 require.NoError(t, err) 779 err = cache.waitForDeletes(ctx) 780 require.NoError(t, err) 781 require.Equal(t, 1, cache.syncCache.numBlocks) 782 require.Equal(t, 0, cache.workingSetCache.numBlocks) 783 784 t.Log("After the move, make sure the prefetch status is downgraded.") 785 _, _, prefetchStatus, err := cache.Get( 786 ctx, tlf1, block1Ptr.ID, DiskBlockAnyCache) 787 require.NoError(t, err) 788 require.Equal(t, 1, cache.syncCache.numBlocks) 789 require.Equal(t, TriggeredPrefetch, prefetchStatus) 790 } 791 792 // seedTlf seeds the cache with blocks from a given TLF ID. Notably, 793 // it does NOT give them different times, 794 // because that makes TLFs filled first more likely to face eviction. 795 func seedTlf(ctx context.Context, t *testing.T, 796 cache *diskBlockCacheWrapped, config diskBlockCacheConfig, tlfID tlf.ID, 797 numBlocksPerTlf int) { 798 for j := 0; j < numBlocksPerTlf; j++ { 799 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 800 t, config) 801 err := cache.Put( 802 ctx, tlfID, blockPtr.ID, blockEncoded, serverHalf, 803 DiskBlockSyncCache) 804 require.NoError(t, err) 805 } 806 807 } 808 809 func TestDiskBlockCacheHomeDirPriorities(t *testing.T) { 810 t.Parallel() 811 t.Log("Test that blocks from a home directory aren't evicted when there" + 812 " are other options.") 813 cache, config := initDiskBlockCacheTest(t) 814 defer shutdownDiskBlockCacheTest(cache) 815 816 ctx := context.Background() 817 818 rand.Seed(1) 819 820 t.Log("Set home directories on the cache") 821 homeTLF := tlf.FakeID(100, tlf.Private) 822 err := cache.AddHomeTLF(ctx, homeTLF) 823 require.NoError(t, err) 824 homePublicTLF := tlf.FakeID(101, tlf.Public) 825 err = cache.AddHomeTLF(ctx, homePublicTLF) 826 require.NoError(t, err) 827 828 t.Log("Seed the cache with blocks") 829 totalBlocks := 0 830 homeTLFBlocksEach := 50 831 originalSizes := map[tlf.ID]int{ 832 homePublicTLF: homeTLFBlocksEach, 833 homeTLF: homeTLFBlocksEach, 834 } 835 836 seedTlf(ctx, t, cache, config, homePublicTLF, homeTLFBlocksEach) 837 seedTlf(ctx, t, cache, config, homeTLF, homeTLFBlocksEach) 838 totalBlocks += 2 * homeTLFBlocksEach 839 otherTlfIds := []tlf.ID{ 840 tlf.FakeID(1, tlf.Private), 841 tlf.FakeID(2, tlf.Public), 842 tlf.FakeID(3, tlf.Private), 843 tlf.FakeID(4, tlf.Private), 844 tlf.FakeID(5, tlf.Public), 845 } 846 847 // Distribute the blocks exponentially over the non-home TLFs. 848 // Use LOTS of blocks to get better statistical behavior. 849 nextTlfSize := 200 850 for _, tlfID := range otherTlfIds { 851 seedTlf(ctx, t, cache, config, tlfID, nextTlfSize) 852 originalSizes[tlfID] = nextTlfSize 853 totalBlocks += nextTlfSize 854 nextTlfSize *= 2 855 } 856 857 t.Log("Evict half the non-home TLF blocks using small eviction sizes.") 858 evictionSize := 5 859 numEvictions := (totalBlocks - 2*homeTLFBlocksEach) / (evictionSize * 2) 860 for i := 0; i < numEvictions; i++ { 861 _, _, err := cache.syncCache.evictLocked(ctx, evictionSize) 862 require.NoError(t, err) 863 totalBlocks -= evictionSize 864 } 865 866 t.Log("Verify that the non-home TLFs have been reduced in size by about" + 867 " half") 868 // Allow a tolerance of .5, so 25-75% of the original size. 869 for _, tlfID := range otherTlfIds { 870 original := originalSizes[tlfID] 871 current := cache.syncCache.tlfCounts[tlfID] 872 t.Logf("ID: %v, Current: %d, Original: %d", tlfID, current, original) 873 require.InEpsilon(t, original/2, current, 0.5) 874 } 875 require.Equal(t, homeTLFBlocksEach, cache.syncCache.tlfCounts[homeTLF]) 876 require.Equal(t, homeTLFBlocksEach, cache.syncCache.tlfCounts[homePublicTLF]) 877 878 t.Log("Evict the rest of the non-home TLF blocks in 2 evictions.") 879 numEvictions = 2 880 evictionSize1 := (totalBlocks - 2*homeTLFBlocksEach) / numEvictions 881 // In the second eviction, evict enough blocks to touch the public home. 882 publicHomeEvict := 10 883 evictionSize2 := totalBlocks - 2*homeTLFBlocksEach - evictionSize1 + publicHomeEvict 884 885 _, _, err = cache.syncCache.evictLocked(ctx, evictionSize1) 886 require.NoError(t, err) 887 888 // Make sure the home TLFs are not touched. 889 require.Equal(t, homeTLFBlocksEach, cache.syncCache.tlfCounts[homeTLF]) 890 require.Equal(t, homeTLFBlocksEach, cache.syncCache.tlfCounts[homePublicTLF]) 891 892 _, _, err = cache.syncCache.evictLocked(ctx, evictionSize2) 893 require.NoError(t, err) 894 895 // Make sure the home TLFs are minimally touched. 896 require.Equal(t, homeTLFBlocksEach, cache.syncCache.tlfCounts[homeTLF]) 897 require.Equal(t, homeTLFBlocksEach-publicHomeEvict, 898 cache.syncCache.tlfCounts[homePublicTLF]) 899 900 t.Log("Evict enough blocks to get rid of the public home TLF.") 901 _, _, err = cache.syncCache.evictLocked(ctx, homeTLFBlocksEach-publicHomeEvict) 902 require.NoError(t, err) 903 require.Equal(t, homeTLFBlocksEach, cache.syncCache.tlfCounts[homeTLF]) 904 require.Equal(t, 0, cache.syncCache.tlfCounts[homePublicTLF]) 905 906 t.Log("Evict enough blocks to get rid of the private home TLF.") 907 _, _, err = cache.syncCache.evictLocked(ctx, homeTLFBlocksEach) 908 require.NoError(t, err) 909 require.Equal(t, 0, cache.syncCache.tlfCounts[homeTLF]) 910 require.Equal(t, 0, cache.syncCache.tlfCounts[homePublicTLF]) 911 } 912 913 func TestDiskBlockCacheMark(t *testing.T) { 914 t.Parallel() 915 t.Log("Test that basic disk cache marking and deleting work.") 916 cache, config := initDiskBlockCacheTest(t) 917 defer shutdownDiskBlockCacheTest(cache) 918 standardCache := cache.syncCache 919 ctx := context.Background() 920 921 t.Log("Insert lots of blocks.") 922 numTlfs := 3 923 numBlocksPerTlf := 5 924 numBlocks := numTlfs * numBlocksPerTlf 925 seedDiskBlockCacheForTest(ctx, t, cache, config, numTlfs, numBlocksPerTlf) 926 require.Equal(t, numBlocks, standardCache.numBlocks) 927 928 t.Log("Generate some blocks we can mark.") 929 tlfID := tlf.FakeID(1, tlf.Private) 930 ids := make([]kbfsblock.ID, numBlocksPerTlf) 931 for i := 0; i < numBlocksPerTlf; i++ { 932 blockPtr, _, blockEncoded, serverHalf := setupBlockForDiskCache( 933 t, config) 934 err := cache.Put( 935 ctx, tlfID, blockPtr.ID, blockEncoded, serverHalf, 936 DiskBlockSyncCache) 937 require.NoError(t, err) 938 ids[i] = blockPtr.ID 939 } 940 numBlocks += numBlocksPerTlf 941 require.Equal(t, numBlocks, standardCache.numBlocks) 942 943 t.Log("Mark a couple blocks.") 944 tag := "mark" 945 err := cache.Mark(ctx, ids[1], tag, DiskBlockSyncCache) 946 require.NoError(t, err) 947 err = cache.Mark(ctx, ids[3], tag, DiskBlockSyncCache) 948 require.NoError(t, err) 949 950 t.Log("Delete all unmarked blocks.") 951 standardCache.clearTickerDuration = 0 952 standardCache.numUnmarkedBlocksToCheck = 1 953 err = cache.DeleteUnmarked(ctx, tlfID, tag, DiskBlockSyncCache) 954 require.NoError(t, err) 955 require.Equal(t, numBlocks-(2*numBlocksPerTlf-2), standardCache.numBlocks) 956 _, _, _, err = cache.Get(ctx, tlfID, ids[0], DiskBlockAnyCache) 957 require.EqualError(t, err, data.NoSuchBlockError{ID: ids[0]}.Error()) 958 _, _, _, err = cache.Get(ctx, tlfID, ids[2], DiskBlockAnyCache) 959 require.EqualError(t, err, data.NoSuchBlockError{ID: ids[2]}.Error()) 960 _, _, _, err = cache.Get(ctx, tlfID, ids[4], DiskBlockAnyCache) 961 require.EqualError(t, err, data.NoSuchBlockError{ID: ids[4]}.Error()) 962 } 963 964 func TestDiskBlockCacheRemoveBrokenBlocks(t *testing.T) { 965 t.Parallel() 966 t.Log("Test that blocks with corrupt metadata are removed.") 967 cache, config := initDiskBlockCacheTest(t) 968 defer shutdownDiskBlockCacheTest(cache) 969 wsCache := cache.workingSetCache 970 ctx := context.Background() 971 972 t.Log("Add one block, then corrupt its metadata.") 973 tlfID := tlf.FakeID(1, tlf.Private) 974 blockPtr1, _, blockEncoded1, serverHalf1 := setupBlockForDiskCache( 975 t, config) 976 err := cache.Put( 977 ctx, tlfID, blockPtr1.ID, blockEncoded1, serverHalf1, 978 DiskBlockWorkingSetCache) 979 require.NoError(t, err) 980 require.Equal(t, 1, wsCache.numBlocks) 981 982 err = wsCache.metaDb.Delete(blockPtr1.ID.Bytes(), nil) 983 require.NoError(t, err) 984 985 t.Log("Make the cache full, and put a new block, which should succeed " + 986 "and will remove the broken block.") 987 currBytes := int64(cache.workingSetCache.currBytes) 988 limiter := config.DiskLimiter().(*backpressureDiskLimiter) 989 limiter.diskCacheByteTracker.limit = currBytes 990 991 blockPtr2, _, blockEncoded2, serverHalf2 := setupBlockForDiskCache( 992 t, config) 993 err = cache.Put( 994 ctx, tlfID, blockPtr2.ID, blockEncoded2, serverHalf2, 995 DiskBlockWorkingSetCache) 996 require.NoError(t, err) 997 require.Equal(t, 1, wsCache.numBlocks) 998 }