github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libkbfs/disk_block_cache_wrapped.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 "path/filepath" 9 "sync" 10 11 "github.com/keybase/client/go/kbfs/data" 12 "github.com/keybase/client/go/kbfs/kbfsblock" 13 "github.com/keybase/client/go/kbfs/kbfscrypto" 14 "github.com/keybase/client/go/kbfs/kbfsmd" 15 "github.com/keybase/client/go/kbfs/kbfssync" 16 "github.com/keybase/client/go/kbfs/tlf" 17 "github.com/pkg/errors" 18 ldberrors "github.com/syndtr/goleveldb/leveldb/errors" 19 "golang.org/x/net/context" 20 ) 21 22 const ( 23 workingSetCacheFolderName = "kbfs_block_cache" 24 syncCacheFolderName = "kbfs_sync_cache" 25 ) 26 27 // diskBlockCacheConfig specifies the interfaces that a DiskBlockCacheStandard 28 // needs to perform its functions. This adheres to the standard libkbfs Config 29 // API. 30 type diskBlockCacheConfig interface { 31 codecGetter 32 logMaker 33 clockGetter 34 diskLimiterGetter 35 initModeGetter 36 blockCacher 37 } 38 39 type diskBlockCacheWrapped struct { 40 config diskBlockCacheConfig 41 storageRoot string 42 // Protects the caches 43 mtx sync.RWMutex 44 workingSetCache *DiskBlockCacheLocal 45 syncCache *DiskBlockCacheLocal 46 deleteGroup kbfssync.RepeatedWaitGroup 47 } 48 49 var _ DiskBlockCache = (*diskBlockCacheWrapped)(nil) 50 51 func (cache *diskBlockCacheWrapped) enableCache( 52 typ diskLimitTrackerType, cacheFolder string, mode InitMode) (err error) { 53 cache.mtx.Lock() 54 defer cache.mtx.Unlock() 55 var cachePtr **DiskBlockCacheLocal 56 switch typ { 57 case syncCacheLimitTrackerType: 58 cachePtr = &cache.syncCache 59 case workingSetCacheLimitTrackerType: 60 cachePtr = &cache.workingSetCache 61 default: 62 return errors.New("invalid disk cache type") 63 } 64 if *cachePtr != nil { 65 // We already have a cache of the desired type. Thus, this method is 66 // idempotent. 67 return nil 68 } 69 if mode.IsTestMode() { 70 *cachePtr, err = newDiskBlockCacheLocalForTest( 71 cache.config, typ) 72 } else { 73 cacheStorageRoot := filepath.Join(cache.storageRoot, cacheFolder) 74 *cachePtr, err = newDiskBlockCacheLocal( 75 cache.config, typ, cacheStorageRoot, mode) 76 } 77 return err 78 } 79 80 func newDiskBlockCacheWrapped( 81 config diskBlockCacheConfig, storageRoot string, mode InitMode) ( 82 cache *diskBlockCacheWrapped, err error) { 83 cache = &diskBlockCacheWrapped{ 84 config: config, 85 storageRoot: storageRoot, 86 } 87 err = cache.enableCache( 88 workingSetCacheLimitTrackerType, workingSetCacheFolderName, mode) 89 if err != nil { 90 return nil, err 91 } 92 syncCacheErr := cache.enableCache( 93 syncCacheLimitTrackerType, syncCacheFolderName, mode) 94 if syncCacheErr != nil { 95 log := config.MakeLogger("DBC") 96 log.Warning("Could not initialize sync block cache.") 97 // We still return success because the working set cache successfully 98 // initialized. 99 } 100 return cache, nil 101 } 102 103 func (cache *diskBlockCacheWrapped) getCacheLocked( 104 cacheType DiskBlockCacheType) (*DiskBlockCacheLocal, error) { 105 if cacheType == DiskBlockSyncCache { 106 if cache.syncCache == nil { 107 return nil, errors.New("Sync cache not enabled") 108 } 109 return cache.syncCache, nil 110 } 111 return cache.workingSetCache, nil 112 } 113 114 // DoesCacheHaveSpace implements the DiskBlockCache interface for 115 // diskBlockCacheWrapped. 116 func (cache *diskBlockCacheWrapped) DoesCacheHaveSpace( 117 ctx context.Context, cacheType DiskBlockCacheType) (bool, int64, error) { 118 cache.mtx.RLock() 119 defer cache.mtx.RUnlock() 120 c, err := cache.getCacheLocked(cacheType) 121 if err != nil { 122 return false, 0, err 123 } 124 return c.DoesCacheHaveSpace(ctx) 125 } 126 127 // IsSyncCacheEnabled returns true if the sync cache is enabled. 128 func (cache *diskBlockCacheWrapped) IsSyncCacheEnabled() bool { 129 return cache.syncCache != nil 130 } 131 132 func (cache *diskBlockCacheWrapped) rankCachesLocked( 133 preferredCacheType DiskBlockCacheType) ( 134 primaryCache, secondaryCache *DiskBlockCacheLocal) { 135 if preferredCacheType != DiskBlockWorkingSetCache { 136 if cache.syncCache == nil { 137 log := cache.config.MakeLogger("DBC") 138 log.Warning("Sync cache is preferred, but there is no sync cache") 139 return cache.workingSetCache, nil 140 } 141 return cache.syncCache, cache.workingSetCache 142 } 143 return cache.workingSetCache, cache.syncCache 144 } 145 146 func (cache *diskBlockCacheWrapped) moveBetweenCachesWithBlockLocked( 147 ctx context.Context, tlfID tlf.ID, blockID kbfsblock.ID, buf []byte, 148 serverHalf kbfscrypto.BlockCryptKeyServerHalf, 149 prefetchStatus PrefetchStatus, newCacheType DiskBlockCacheType) { 150 primaryCache, secondaryCache := cache.rankCachesLocked(newCacheType) 151 // Move the block into its preferred cache. 152 err := primaryCache.Put(ctx, tlfID, blockID, buf, serverHalf) 153 if err != nil { 154 // The cache will log the non-fatal error, so just return. 155 return 156 } 157 158 if prefetchStatus == FinishedPrefetch { 159 // Don't propagate a finished status to the primary 160 // cache, since the status needs to be with respect to 161 // that particular cache (i.e., if the primary cache 162 // is the sync cache, all the child blocks must be in 163 // the sync cache, for this block to be considered 164 // synced, and we can't verify that here). 165 prefetchStatus = TriggeredPrefetch 166 } 167 if prefetchStatus != NoPrefetch { 168 _ = primaryCache.UpdateMetadata(ctx, blockID, prefetchStatus) 169 } 170 171 // Remove the block from the non-preferred cache (which is 172 // set to be the secondary cache at this point). 173 cache.deleteGroup.Add(1) 174 go func() { 175 defer cache.deleteGroup.Done() 176 // Don't catch the errors -- this is just best effort. 177 _, _, _ = secondaryCache.Delete(ctx, []kbfsblock.ID{blockID}) 178 }() 179 } 180 181 // Get implements the DiskBlockCache interface for diskBlockCacheWrapped. 182 func (cache *diskBlockCacheWrapped) Get( 183 ctx context.Context, tlfID tlf.ID, blockID kbfsblock.ID, 184 preferredCacheType DiskBlockCacheType) ( 185 buf []byte, serverHalf kbfscrypto.BlockCryptKeyServerHalf, 186 prefetchStatus PrefetchStatus, err error) { 187 cache.mtx.RLock() 188 defer cache.mtx.RUnlock() 189 primaryCache, secondaryCache := cache.rankCachesLocked(preferredCacheType) 190 // Check both caches if the primary cache doesn't have the block. 191 buf, serverHalf, prefetchStatus, err = primaryCache.Get(ctx, tlfID, blockID) 192 if _, isNoSuchBlockError := errors.Cause(err).(data.NoSuchBlockError); isNoSuchBlockError && 193 secondaryCache != nil { 194 buf, serverHalf, prefetchStatus, err = secondaryCache.Get( 195 ctx, tlfID, blockID) 196 if err != nil { 197 return nil, kbfscrypto.BlockCryptKeyServerHalf{}, NoPrefetch, err 198 } 199 if preferredCacheType != DiskBlockAnyCache { 200 cache.moveBetweenCachesWithBlockLocked( 201 ctx, tlfID, blockID, buf, serverHalf, prefetchStatus, 202 preferredCacheType) 203 } 204 } 205 return buf, serverHalf, prefetchStatus, err 206 } 207 208 // GetMetadata implements the DiskBlockCache interface for 209 // diskBlockCacheWrapped. 210 func (cache *diskBlockCacheWrapped) GetMetadata(ctx context.Context, 211 blockID kbfsblock.ID) (metadata DiskBlockCacheMetadata, err error) { 212 cache.mtx.RLock() 213 defer cache.mtx.RUnlock() 214 if cache.syncCache != nil { 215 md, err := cache.syncCache.GetMetadata(ctx, blockID) 216 switch errors.Cause(err) { 217 case nil: 218 return md, nil 219 case ldberrors.ErrNotFound: 220 default: 221 return md, err 222 } 223 } 224 return cache.workingSetCache.GetMetadata(ctx, blockID) 225 } 226 227 func (cache *diskBlockCacheWrapped) moveBetweenCachesLocked( 228 ctx context.Context, tlfID tlf.ID, blockID kbfsblock.ID, 229 newCacheType DiskBlockCacheType) (moved bool) { 230 _, secondaryCache := cache.rankCachesLocked(newCacheType) 231 buf, serverHalf, prefetchStatus, err := secondaryCache.Get( 232 ctx, tlfID, blockID) 233 if err != nil { 234 // The block isn't in the secondary cache, so there's nothing to move. 235 return false 236 } 237 cache.moveBetweenCachesWithBlockLocked( 238 ctx, tlfID, blockID, buf, serverHalf, prefetchStatus, newCacheType) 239 return true 240 } 241 242 // GetPefetchStatus implements the DiskBlockCache interface for 243 // diskBlockCacheWrapped. 244 func (cache *diskBlockCacheWrapped) GetPrefetchStatus( 245 ctx context.Context, tlfID tlf.ID, blockID kbfsblock.ID, 246 cacheType DiskBlockCacheType) (prefetchStatus PrefetchStatus, err error) { 247 cache.mtx.RLock() 248 defer cache.mtx.RUnlock() 249 250 // Try the sync cache first unless working set cache is required. 251 if cacheType != DiskBlockWorkingSetCache { 252 if cache.syncCache == nil { 253 return NoPrefetch, errors.New("Sync cache not enabled") 254 } 255 256 md, err := cache.syncCache.GetMetadata(ctx, blockID) 257 switch errors.Cause(err) { 258 case nil: 259 return md.PrefetchStatus(), nil 260 case ldberrors.ErrNotFound: 261 if cacheType == DiskBlockSyncCache { 262 // Try moving the block and getting it again. 263 moved := cache.moveBetweenCachesLocked( 264 ctx, tlfID, blockID, cacheType) 265 if moved { 266 md, err := cache.syncCache.GetMetadata(ctx, blockID) 267 if err != nil { 268 return NoPrefetch, err 269 } 270 return md.PrefetchStatus(), nil 271 } 272 return NoPrefetch, err 273 } 274 // Otherwise try the working set cache below. 275 default: 276 return NoPrefetch, err 277 } 278 } 279 280 md, err := cache.workingSetCache.GetMetadata(ctx, blockID) 281 if err != nil { 282 return NoPrefetch, err 283 } 284 return md.PrefetchStatus(), nil 285 } 286 287 // Put implements the DiskBlockCache interface for diskBlockCacheWrapped. 288 func (cache *diskBlockCacheWrapped) Put(ctx context.Context, tlfID tlf.ID, 289 blockID kbfsblock.ID, buf []byte, 290 serverHalf kbfscrypto.BlockCryptKeyServerHalf, 291 cacheType DiskBlockCacheType) error { 292 // This is a write operation but we are only reading the pointers to the 293 // caches. So we use a read lock. 294 cache.mtx.RLock() 295 defer cache.mtx.RUnlock() 296 if cacheType == DiskBlockSyncCache && cache.syncCache != nil { 297 workingSetCache := cache.workingSetCache 298 err := cache.syncCache.Put(ctx, tlfID, blockID, buf, serverHalf) 299 if err == nil { 300 cache.deleteGroup.Add(1) 301 go func() { 302 defer cache.deleteGroup.Done() 303 // Don't catch the errors -- this is just best effort. 304 _, _, _ = workingSetCache.Delete(ctx, []kbfsblock.ID{blockID}) 305 }() 306 return nil 307 } 308 // Otherwise drop through and put it into the working set cache. 309 } 310 // No need to put it in the working cache if it's already in the 311 // sync cache. 312 if cache.syncCache != nil { 313 _, _, _, err := cache.syncCache.Get(ctx, tlfID, blockID) 314 if err == nil { 315 return nil 316 } 317 } 318 return cache.workingSetCache.Put(ctx, tlfID, blockID, buf, serverHalf) 319 } 320 321 // Delete implements the DiskBlockCache interface for diskBlockCacheWrapped. 322 func (cache *diskBlockCacheWrapped) Delete(ctx context.Context, 323 blockIDs []kbfsblock.ID, cacheType DiskBlockCacheType) ( 324 numRemoved int, sizeRemoved int64, err error) { 325 // This is a write operation but we are only reading the pointers to the 326 // caches. So we use a read lock. 327 cache.mtx.RLock() 328 defer cache.mtx.RUnlock() 329 if cache.syncCache == nil && cacheType == DiskBlockSyncCache { 330 return 0, 0, errors.New("Sync cache not enabled") 331 } else if cache.syncCache != nil && 332 (cacheType == DiskBlockAnyCache || cacheType == DiskBlockSyncCache) { 333 numRemoved, sizeRemoved, err = cache.syncCache.Delete(ctx, blockIDs) 334 if err != nil { 335 return 0, 0, err 336 } 337 if cacheType == DiskBlockSyncCache { 338 return numRemoved, sizeRemoved, err 339 } 340 } 341 342 wsNumRemoved, wsSizeRemoved, err := cache.workingSetCache.Delete( 343 ctx, blockIDs) 344 if err != nil { 345 return 0, 0, err 346 } 347 return wsNumRemoved + numRemoved, wsSizeRemoved + sizeRemoved, nil 348 } 349 350 // UpdateMetadata implements the DiskBlockCache interface for 351 // diskBlockCacheWrapped. 352 func (cache *diskBlockCacheWrapped) UpdateMetadata( 353 ctx context.Context, tlfID tlf.ID, blockID kbfsblock.ID, 354 prefetchStatus PrefetchStatus, cacheType DiskBlockCacheType) error { 355 // This is a write operation but we are only reading the pointers to the 356 // caches. So we use a read lock. 357 cache.mtx.RLock() 358 defer cache.mtx.RUnlock() 359 primaryCache, secondaryCache := cache.rankCachesLocked(cacheType) 360 361 err := primaryCache.UpdateMetadata(ctx, blockID, prefetchStatus) 362 _, isNoSuchBlockError := errors.Cause(err).(data.NoSuchBlockError) 363 if !isNoSuchBlockError { 364 return err 365 } 366 if cacheType == DiskBlockSyncCache { 367 // Try moving the block and getting it again. 368 moved := cache.moveBetweenCachesLocked( 369 ctx, tlfID, blockID, cacheType) 370 if moved { 371 err = primaryCache.UpdateMetadata(ctx, blockID, prefetchStatus) 372 } 373 return err 374 } 375 err = secondaryCache.UpdateMetadata(ctx, blockID, prefetchStatus) 376 _, isNoSuchBlockError = errors.Cause(err).(data.NoSuchBlockError) 377 if !isNoSuchBlockError { 378 return err 379 } 380 // Try one last time in the primary cache, in case of this 381 // sequence of events: 382 // 0) Block exists in secondary. 383 // 1) UpdateMetadata checks primary, gets NoSuchBlockError. 384 // 2) Other goroutine writes block to primary. 385 // 3) Other goroutine deletes block from primary. 386 // 4) UpdateMetadata checks secondary, gets NoSuchBlockError. 387 return primaryCache.UpdateMetadata(ctx, blockID, prefetchStatus) 388 } 389 390 // ClearAllTlfBlocks implements the DiskBlockCache interface for 391 // diskBlockCacheWrapper. 392 func (cache *diskBlockCacheWrapped) ClearAllTlfBlocks( 393 ctx context.Context, tlfID tlf.ID, cacheType DiskBlockCacheType) error { 394 cache.mtx.RLock() 395 defer cache.mtx.RUnlock() 396 c, err := cache.getCacheLocked(cacheType) 397 if err != nil { 398 return err 399 } 400 return c.ClearAllTlfBlocks(ctx, tlfID) 401 } 402 403 // GetLastUnrefRev implements the DiskBlockCache interface for 404 // diskBlockCacheWrapped. 405 func (cache *diskBlockCacheWrapped) GetLastUnrefRev( 406 ctx context.Context, tlfID tlf.ID, cacheType DiskBlockCacheType) ( 407 kbfsmd.Revision, error) { 408 cache.mtx.RLock() 409 defer cache.mtx.RUnlock() 410 c, err := cache.getCacheLocked(cacheType) 411 if err != nil { 412 return kbfsmd.RevisionUninitialized, err 413 } 414 return c.GetLastUnrefRev(ctx, tlfID) 415 } 416 417 // PutLastUnrefRev implements the DiskBlockCache interface for 418 // diskBlockCacheWrapped. 419 func (cache *diskBlockCacheWrapped) PutLastUnrefRev( 420 ctx context.Context, tlfID tlf.ID, rev kbfsmd.Revision, 421 cacheType DiskBlockCacheType) error { 422 cache.mtx.RLock() 423 defer cache.mtx.RUnlock() 424 c, err := cache.getCacheLocked(cacheType) 425 if err != nil { 426 return err 427 } 428 return c.PutLastUnrefRev(ctx, tlfID, rev) 429 } 430 431 // Status implements the DiskBlockCache interface for diskBlockCacheWrapped. 432 func (cache *diskBlockCacheWrapped) Status( 433 ctx context.Context) map[string]DiskBlockCacheStatus { 434 // This is a write operation but we are only reading the pointers to the 435 // caches. So we use a read lock. 436 cache.mtx.RLock() 437 defer cache.mtx.RUnlock() 438 statuses := make(map[string]DiskBlockCacheStatus, 2) 439 if cache.workingSetCache != nil { 440 for name, status := range cache.workingSetCache.Status(ctx) { 441 statuses[name] = status 442 } 443 } 444 if cache.syncCache == nil { 445 return statuses 446 } 447 for name, status := range cache.syncCache.Status(ctx) { 448 statuses[name] = status 449 } 450 return statuses 451 } 452 453 // Mark implements the DiskBlockCache interface for diskBlockCacheWrapped. 454 func (cache *diskBlockCacheWrapped) Mark( 455 ctx context.Context, blockID kbfsblock.ID, tag string, 456 cacheType DiskBlockCacheType) error { 457 cache.mtx.RLock() 458 defer cache.mtx.RUnlock() 459 c, err := cache.getCacheLocked(cacheType) 460 if err != nil { 461 return err 462 } 463 return c.Mark(ctx, blockID, tag) 464 } 465 466 // DeleteUnmarked implements the DiskBlockCache interface for 467 // diskBlockCacheWrapped. 468 func (cache *diskBlockCacheWrapped) DeleteUnmarked( 469 ctx context.Context, tlfID tlf.ID, tag string, 470 cacheType DiskBlockCacheType) error { 471 cache.mtx.RLock() 472 defer cache.mtx.RUnlock() 473 c, err := cache.getCacheLocked(cacheType) 474 if err != nil { 475 return err 476 } 477 return c.DeleteUnmarked(ctx, tlfID, tag) 478 } 479 480 func (cache *diskBlockCacheWrapped) waitForDeletes(ctx context.Context) error { 481 return cache.deleteGroup.Wait(ctx) 482 } 483 484 // AddHomeTLF implements the DiskBlockCache interface for diskBlockCacheWrapped. 485 func (cache *diskBlockCacheWrapped) AddHomeTLF(ctx context.Context, 486 tlfID tlf.ID) error { 487 cache.mtx.RLock() 488 defer cache.mtx.RUnlock() 489 if cache.syncCache == nil { 490 return errors.New("Sync cache not enabled") 491 } 492 return cache.syncCache.AddHomeTLF(ctx, tlfID) 493 } 494 495 // ClearHomeTLFs implements the DiskBlockCache interface for 496 // diskBlockCacheWrapped. 497 func (cache *diskBlockCacheWrapped) ClearHomeTLFs(ctx context.Context) error { 498 cache.mtx.RLock() 499 defer cache.mtx.RUnlock() 500 if cache.syncCache == nil { 501 return errors.New("Sync cache not enabled") 502 } 503 return cache.syncCache.ClearHomeTLFs(ctx) 504 } 505 506 // GetTlfSize implements the DiskBlockCache interface for 507 // diskBlockCacheWrapped. 508 func (cache *diskBlockCacheWrapped) GetTlfSize( 509 ctx context.Context, tlfID tlf.ID, cacheType DiskBlockCacheType) ( 510 size uint64, err error) { 511 cache.mtx.RLock() 512 defer cache.mtx.RUnlock() 513 514 if cacheType != DiskBlockWorkingSetCache && cache.syncCache != nil { 515 // Either sync cache only, or both. 516 syncSize, err := cache.syncCache.GetTlfSize(ctx, tlfID) 517 if err != nil { 518 return 0, err 519 } 520 size += syncSize 521 } 522 523 if cacheType != DiskBlockSyncCache { 524 // Either working set cache only, or both. 525 workingSetSize, err := cache.workingSetCache.GetTlfSize(ctx, tlfID) 526 if err != nil { 527 return 0, err 528 } 529 size += workingSetSize 530 } 531 532 return size, nil 533 } 534 535 // GetTlfSize implements the DiskBlockCache interface for 536 // diskBlockCacheWrapped. 537 func (cache *diskBlockCacheWrapped) GetTlfIDs( 538 ctx context.Context, cacheType DiskBlockCacheType) ( 539 tlfIDs []tlf.ID, err error) { 540 cache.mtx.RLock() 541 defer cache.mtx.RUnlock() 542 543 if cacheType != DiskBlockWorkingSetCache && cache.syncCache != nil { 544 // Either sync cache only, or both. 545 tlfIDs, err = cache.syncCache.GetTlfIDs(ctx) 546 if err != nil { 547 return nil, err 548 } 549 } 550 551 if cacheType != DiskBlockSyncCache { 552 // Either working set cache only, or both. 553 wsTlfIDs, err := cache.workingSetCache.GetTlfIDs(ctx) 554 if err != nil { 555 return nil, err 556 } 557 558 // Uniquify them if needed. 559 if len(tlfIDs) == 0 { 560 tlfIDs = wsTlfIDs 561 } else { 562 s := make(map[tlf.ID]bool, len(tlfIDs)+len(wsTlfIDs)) 563 for _, id := range tlfIDs { 564 s[id] = true 565 } 566 for _, id := range wsTlfIDs { 567 s[id] = true 568 } 569 tlfIDs = make([]tlf.ID, 0, len(s)) 570 for id := range s { 571 tlfIDs = append(tlfIDs, id) 572 } 573 } 574 } 575 576 return tlfIDs, nil 577 } 578 579 // WaitUntilStarted implements the DiskBlockCache interface for 580 // diskBlockCacheWrapped. 581 func (cache *diskBlockCacheWrapped) WaitUntilStarted( 582 cacheType DiskBlockCacheType) (err error) { 583 cache.mtx.RLock() 584 defer cache.mtx.RUnlock() 585 586 if cacheType != DiskBlockWorkingSetCache && cache.syncCache != nil { 587 err = cache.syncCache.WaitUntilStarted() 588 if err != nil { 589 return err 590 } 591 } 592 593 if cacheType != DiskBlockSyncCache { 594 err = cache.workingSetCache.WaitUntilStarted() 595 if err != nil { 596 return err 597 } 598 } 599 600 return nil 601 } 602 603 // Shutdown implements the DiskBlockCache interface for diskBlockCacheWrapped. 604 func (cache *diskBlockCacheWrapped) Shutdown(ctx context.Context) <-chan struct{} { 605 cache.mtx.Lock() 606 defer cache.mtx.Unlock() 607 wsCh := cache.workingSetCache.Shutdown(ctx) 608 var syncCh <-chan struct{} 609 if cache.syncCache != nil { 610 syncCh = cache.syncCache.Shutdown(ctx) 611 } else { 612 ch := make(chan struct{}) 613 close(ch) 614 syncCh = ch 615 } 616 retCh := make(chan struct{}) 617 go func() { 618 <-wsCh 619 <-syncCh 620 close(retCh) 621 }() 622 return retCh 623 }