github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/block_retrieval_worker_test.go (about)

     1  // Copyright 2016 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  package libkbfs
     5  
     6  import (
     7  	"errors"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/kbfs/data"
    13  	"github.com/keybase/client/go/kbfs/env"
    14  	"github.com/keybase/client/go/kbfs/kbfscodec"
    15  	"github.com/keybase/client/go/kbfs/kbfscrypto"
    16  	"github.com/keybase/client/go/kbfs/libkey"
    17  	"github.com/stretchr/testify/require"
    18  	"golang.org/x/net/context"
    19  )
    20  
    21  // blockReturner contains a block value to copy into requested blocks, and a
    22  // channel to synchronize on with the worker.
    23  type blockReturner struct {
    24  	block      data.Block
    25  	continueCh chan error
    26  	startCh    chan struct{}
    27  }
    28  
    29  // fakeBlockGetter allows specifying and obtaining fake blocks.
    30  type fakeBlockGetter struct {
    31  	mtx           sync.RWMutex
    32  	blockMap      map[data.BlockPointer]blockReturner
    33  	codec         kbfscodec.Codec
    34  	respectCancel bool
    35  }
    36  
    37  // newFakeBlockGetter returns a fakeBlockGetter.
    38  func newFakeBlockGetter(respectCancel bool) *fakeBlockGetter {
    39  	return &fakeBlockGetter{
    40  		blockMap:      make(map[data.BlockPointer]blockReturner),
    41  		codec:         kbfscodec.NewMsgpack(),
    42  		respectCancel: respectCancel,
    43  	}
    44  }
    45  
    46  // setBlockToReturn sets the block that will be returned for a given
    47  // BlockPointer. Returns a writeable channel that getBlock will wait on, to
    48  // allow synchronization of tests.
    49  func (bg *fakeBlockGetter) setBlockToReturn(blockPtr data.BlockPointer,
    50  	block data.Block) (startCh <-chan struct{}, continueCh chan<- error) {
    51  	bg.mtx.Lock()
    52  	defer bg.mtx.Unlock()
    53  	sCh, cCh := make(chan struct{}), make(chan error)
    54  	bg.blockMap[blockPtr] = blockReturner{
    55  		block:      block,
    56  		startCh:    sCh,
    57  		continueCh: cCh,
    58  	}
    59  	return sCh, cCh
    60  }
    61  
    62  // getBlock implements the interface for realBlockGetter.
    63  func (bg *fakeBlockGetter) getBlock(
    64  	ctx context.Context, kmd libkey.KeyMetadata, blockPtr data.BlockPointer,
    65  	block data.Block, _ DiskBlockCacheType) error {
    66  	bg.mtx.RLock()
    67  	defer bg.mtx.RUnlock()
    68  	source, ok := bg.blockMap[blockPtr]
    69  	if !ok {
    70  		return errors.New("Block doesn't exist in fake block map")
    71  	}
    72  	cancelCh := make(chan struct{})
    73  	if bg.respectCancel {
    74  		go func() {
    75  			<-ctx.Done()
    76  			close(cancelCh)
    77  		}()
    78  	}
    79  	// Wait until the caller tells us to continue
    80  	for {
    81  		select {
    82  		case source.startCh <- struct{}{}:
    83  		case err := <-source.continueCh:
    84  			if err != nil {
    85  				return err
    86  			}
    87  			block.Set(source.block)
    88  			return nil
    89  		case <-cancelCh:
    90  			return ctx.Err()
    91  		}
    92  	}
    93  }
    94  
    95  func (bg *fakeBlockGetter) assembleBlock(ctx context.Context,
    96  	kmd libkey.KeyMetadata, ptr data.BlockPointer, block data.Block, buf []byte,
    97  	serverHalf kbfscrypto.BlockCryptKeyServerHalf) error {
    98  	bg.mtx.RLock()
    99  	defer bg.mtx.RUnlock()
   100  	source, ok := bg.blockMap[ptr]
   101  	if !ok {
   102  		return errors.New("Block doesn't exist in fake block map")
   103  	}
   104  	block.Set(source.block)
   105  	return nil
   106  }
   107  
   108  func (bg *fakeBlockGetter) assembleBlockLocal(ctx context.Context,
   109  	kmd libkey.KeyMetadata, ptr data.BlockPointer, block data.Block, buf []byte,
   110  	serverHalf kbfscrypto.BlockCryptKeyServerHalf) error {
   111  	return bg.assembleBlock(ctx, kmd, ptr, block, buf, serverHalf)
   112  }
   113  
   114  func TestBlockRetrievalWorkerBasic(t *testing.T) {
   115  	t.Log("Test the basic ability of a worker to return a block.")
   116  	bg := newFakeBlockGetter(false)
   117  	q := newBlockRetrievalQueue(
   118  		0, 1, 0, newTestBlockRetrievalConfig(t, bg, nil),
   119  		env.EmptyAppStateUpdater{})
   120  	require.NotNil(t, q)
   121  	defer endBlockRetrievalQueueTest(t, q)
   122  
   123  	ptr1 := makeRandomBlockPointer(t)
   124  	block1 := makeFakeFileBlock(t, false)
   125  	_, continueCh1 := bg.setBlockToReturn(ptr1, block1)
   126  
   127  	block := &data.FileBlock{}
   128  	ch := q.Request(
   129  		context.Background(), 1, makeKMD(), ptr1, block,
   130  		data.NoCacheEntry, BlockRequestSolo)
   131  	continueCh1 <- nil
   132  	err := <-ch
   133  	require.NoError(t, err)
   134  	require.Equal(t, block1, block)
   135  }
   136  
   137  func TestBlockRetrievalWorkerBasicSoloCached(t *testing.T) {
   138  	t.Log("Test the worker fetching and caching a solo block.")
   139  	bg := newFakeBlockGetter(false)
   140  	q := newBlockRetrievalQueue(
   141  		0, 1, 0, newTestBlockRetrievalConfig(t, bg, nil),
   142  		env.EmptyAppStateUpdater{})
   143  	require.NotNil(t, q)
   144  	defer endBlockRetrievalQueueTest(t, q)
   145  
   146  	ptr1 := makeRandomBlockPointer(t)
   147  	block1 := makeFakeFileBlock(t, false)
   148  	_, continueCh1 := bg.setBlockToReturn(ptr1, block1)
   149  
   150  	block := &data.FileBlock{}
   151  	ch := q.Request(
   152  		context.Background(), 1, makeKMD(), ptr1, block, data.TransientEntry,
   153  		BlockRequestSolo)
   154  	continueCh1 <- nil
   155  	err := <-ch
   156  	require.NoError(t, err)
   157  
   158  	_, err = q.config.BlockCache().Get(ptr1)
   159  	require.NoError(t, err)
   160  }
   161  
   162  func TestBlockRetrievalWorkerMultipleWorkers(t *testing.T) {
   163  	t.Log("Test the ability of multiple workers to retrieve concurrently.")
   164  	bg := newFakeBlockGetter(false)
   165  	q := newBlockRetrievalQueue(
   166  		2, 0, 0, newTestBlockRetrievalConfig(t, bg, nil),
   167  		env.EmptyAppStateUpdater{})
   168  	require.NotNil(t, q)
   169  	defer endBlockRetrievalQueueTest(t, q)
   170  
   171  	ptr1, ptr2 := makeRandomBlockPointer(t), makeRandomBlockPointer(t)
   172  	block1, block2 := makeFakeFileBlock(t, false), makeFakeFileBlock(t, false)
   173  	_, continueCh1 := bg.setBlockToReturn(ptr1, block1)
   174  	_, continueCh2 := bg.setBlockToReturn(ptr2, block2)
   175  
   176  	t.Log("Make 2 requests for 2 different blocks")
   177  	block := &data.FileBlock{}
   178  	// Set the base priority to be above the default on-demand
   179  	// fetching, so that the pre-prefetch request for a block doesn't
   180  	// override the other blocks' requests.
   181  	basePriority := defaultOnDemandRequestPriority + 1
   182  	req1Ch := q.Request(
   183  		context.Background(), basePriority, makeKMD(), ptr1, block,
   184  		data.NoCacheEntry, BlockRequestSolo)
   185  	req2Ch := q.Request(
   186  		context.Background(), basePriority, makeKMD(), ptr2, block,
   187  		data.NoCacheEntry, BlockRequestSolo)
   188  
   189  	t.Log("Allow the second request to complete before the first")
   190  	continueCh2 <- nil
   191  	err := <-req2Ch
   192  	require.NoError(t, err)
   193  	require.Equal(t, block2, block)
   194  
   195  	t.Log("Make another request for ptr2")
   196  	req2Ch = q.Request(
   197  		context.Background(), basePriority, makeKMD(), ptr2, block,
   198  		data.NoCacheEntry, BlockRequestSolo)
   199  	continueCh2 <- nil
   200  	err = <-req2Ch
   201  	require.NoError(t, err)
   202  	require.Equal(t, block2, block)
   203  
   204  	t.Log("Complete the ptr1 request")
   205  	continueCh1 <- nil
   206  	err = <-req1Ch
   207  	require.NoError(t, err)
   208  	require.Equal(t, block1, block)
   209  }
   210  
   211  func TestBlockRetrievalWorkerWithQueue(t *testing.T) {
   212  	t.Log("Test the ability of a worker and queue to work correctly together.")
   213  	bg := newFakeBlockGetter(false)
   214  	q := newBlockRetrievalQueue(
   215  		1, 0, 0, newTestBlockRetrievalConfig(t, bg, nil),
   216  		env.EmptyAppStateUpdater{})
   217  	require.NotNil(t, q)
   218  	defer endBlockRetrievalQueueTest(t, q)
   219  
   220  	ptr1, ptr2, ptr3 := makeRandomBlockPointer(t), makeRandomBlockPointer(t),
   221  		makeRandomBlockPointer(t)
   222  	block1, block2, block3 := makeFakeFileBlock(t, false),
   223  		makeFakeFileBlock(t, false), makeFakeFileBlock(t, false)
   224  	startCh1, continueCh1 := bg.setBlockToReturn(ptr1, block1)
   225  	_, continueCh2 := bg.setBlockToReturn(ptr2, block2)
   226  	_, continueCh3 := bg.setBlockToReturn(ptr3, block3)
   227  
   228  	t.Log("Make 3 retrievals for 3 different blocks. All retrievals after " +
   229  		"the first should be queued.")
   230  	block := &data.FileBlock{}
   231  	testBlock1 := &data.FileBlock{}
   232  	testBlock2 := &data.FileBlock{}
   233  	// Set the base priority to be above the default on-demand
   234  	// fetching, so that the pre-prefetch request for a block doesn't
   235  	// override the other blocks' requests.
   236  	basePriority := defaultOnDemandRequestPriority + 1
   237  	req1Ch := q.Request(
   238  		context.Background(), basePriority, makeKMD(), ptr1,
   239  		block, data.NoCacheEntry, BlockRequestSolo)
   240  	req2Ch := q.Request(
   241  		context.Background(), basePriority, makeKMD(), ptr2,
   242  		block, data.NoCacheEntry, BlockRequestSolo)
   243  	req3Ch := q.Request(
   244  		context.Background(), basePriority, makeKMD(), ptr3, testBlock1,
   245  		data.NoCacheEntry, BlockRequestSolo)
   246  	// Ensure the worker picks up the first request
   247  	<-startCh1
   248  	t.Log("Make a high priority request for the third block, which should " +
   249  		"complete next.")
   250  	req4Ch := q.Request(
   251  		context.Background(), basePriority+1, makeKMD(), ptr3, testBlock2,
   252  		data.NoCacheEntry, BlockRequestSolo)
   253  
   254  	t.Log("Allow the ptr1 retrieval to complete.")
   255  	continueCh1 <- nil
   256  	err := <-req1Ch
   257  	require.NoError(t, err)
   258  	require.Equal(t, block1, block)
   259  
   260  	t.Log("Allow the ptr3 retrieval to complete. Both waiting requests " +
   261  		"should complete.")
   262  	continueCh3 <- nil
   263  	err1 := <-req3Ch
   264  	err2 := <-req4Ch
   265  	require.NoError(t, err1)
   266  	require.NoError(t, err2)
   267  	require.Equal(t, block3, testBlock1)
   268  	require.Equal(t, block3, testBlock2)
   269  
   270  	t.Log("Complete the ptr2 retrieval.")
   271  	continueCh2 <- nil
   272  	err = <-req2Ch
   273  	require.NoError(t, err)
   274  	require.Equal(t, block2, block)
   275  }
   276  
   277  func TestBlockRetrievalWorkerCancel(t *testing.T) {
   278  	t.Log("Test the ability of a worker to handle a request cancelation.")
   279  	bg := newFakeBlockGetter(true)
   280  	q := newBlockRetrievalQueue(
   281  		0, 1, 0, newTestBlockRetrievalConfig(t, bg, nil),
   282  		env.EmptyAppStateUpdater{})
   283  	require.NotNil(t, q)
   284  	defer endBlockRetrievalQueueTest(t, q)
   285  
   286  	ptr1 := makeRandomBlockPointer(t)
   287  	block1 := makeFakeFileBlock(t, false)
   288  	// Don't need continueCh here.
   289  	_, _ = bg.setBlockToReturn(ptr1, block1)
   290  
   291  	block := &data.FileBlock{}
   292  	ctx, cancel := context.WithCancel(context.Background())
   293  	cancel()
   294  	ch := q.Request(
   295  		ctx, 1, makeKMD(), ptr1, block, data.NoCacheEntry, BlockRequestSolo)
   296  	err := <-ch
   297  	require.EqualError(t, err, context.Canceled.Error())
   298  }
   299  
   300  func TestBlockRetrievalWorkerShutdown(t *testing.T) {
   301  	t.Log("Test that worker shutdown works.")
   302  	bg := newFakeBlockGetter(false)
   303  	q := newBlockRetrievalQueue(
   304  		1, 0, 0, newTestBlockRetrievalConfig(t, bg, nil),
   305  		env.EmptyAppStateUpdater{})
   306  	require.NotNil(t, q)
   307  	defer endBlockRetrievalQueueTest(t, q)
   308  
   309  	w := q.workers[0]
   310  	require.NotNil(t, w)
   311  
   312  	ptr1 := makeRandomBlockPointer(t)
   313  	block1 := makeFakeFileBlock(t, false)
   314  	_, continueCh := bg.setBlockToReturn(ptr1, block1)
   315  
   316  	w.Shutdown()
   317  	block := &data.FileBlock{}
   318  	ctx, cancel := context.WithCancel(context.Background())
   319  	// Ensure the context loop is stopped so the test doesn't leak goroutines
   320  	defer cancel()
   321  	ch := q.Request(
   322  		ctx, 1, makeKMD(), ptr1, block, data.NoCacheEntry, BlockRequestSolo)
   323  	shutdown := false
   324  	select {
   325  	case <-ch:
   326  		t.Fatal("Expected not to retrieve a result from the Request.")
   327  	case continueCh <- nil:
   328  		t.Fatal("Expected the block getter not to be receiving.")
   329  	default:
   330  		shutdown = true
   331  	}
   332  	require.True(t, shutdown)
   333  
   334  	// Ensure the test completes in a reasonable time.
   335  	timer := time.NewTimer(10 * time.Second)
   336  	doneCh := make(chan struct{})
   337  	go func() {
   338  		w.Shutdown()
   339  		close(doneCh)
   340  	}()
   341  	select {
   342  	case <-timer.C:
   343  		t.Fatal("Expected another Shutdown not to block.")
   344  	case <-doneCh:
   345  	}
   346  }
   347  
   348  func TestBlockRetrievalWorkerPrefetchedPriorityElevation(t *testing.T) {
   349  	t.Log("Test that we can escalate the priority of a request and it " +
   350  		"correctly switches workers.")
   351  	bg := newFakeBlockGetter(false)
   352  	q := newBlockRetrievalQueue(
   353  		1, 1, 0, newTestBlockRetrievalConfig(t, bg, nil),
   354  		env.EmptyAppStateUpdater{})
   355  	require.NotNil(t, q)
   356  	defer endBlockRetrievalQueueTest(t, q)
   357  
   358  	t.Log("Setup source blocks")
   359  	ptr1, ptr2 := makeRandomBlockPointer(t), makeRandomBlockPointer(t)
   360  	block1, block2 := makeFakeFileBlock(t, false), makeFakeFileBlock(t, false)
   361  	_, continueCh1 := bg.setBlockToReturn(ptr1, block1)
   362  	_, continueCh2 := bg.setBlockToReturn(ptr2, block2)
   363  
   364  	t.Log("Make a low-priority request. This will get to the worker.")
   365  	testBlock1 := &data.FileBlock{}
   366  	req1Ch := q.Request(
   367  		context.Background(), 1, makeKMD(), ptr1, testBlock1,
   368  		data.NoCacheEntry, BlockRequestSolo)
   369  
   370  	t.Log("Make another low-priority request. This will block.")
   371  	testBlock2 := &data.FileBlock{}
   372  	req2Ch := q.Request(
   373  		context.Background(), 1, makeKMD(), ptr2, testBlock2,
   374  		data.NoCacheEntry, BlockRequestSolo)
   375  
   376  	t.Log("Make an on-demand request for the same block as the blocked " +
   377  		"request.")
   378  	testBlock3 := &data.FileBlock{}
   379  	req3Ch := q.Request(
   380  		context.Background(), defaultOnDemandRequestPriority,
   381  		makeKMD(), ptr2, testBlock3, data.NoCacheEntry, BlockRequestSolo)
   382  
   383  	t.Log("Release the requests for the second block first. " +
   384  		"Since the prefetch worker is still blocked, this confirms that the " +
   385  		"escalation to an on-demand worker was successful.")
   386  	continueCh2 <- nil
   387  	err := <-req3Ch
   388  	require.NoError(t, err)
   389  	require.Equal(t, testBlock3, block2)
   390  	err = <-req2Ch
   391  	require.NoError(t, err)
   392  	require.Equal(t, testBlock2, block2)
   393  
   394  	t.Log("Allow the initial ptr1 request to complete.")
   395  	continueCh1 <- nil
   396  	err = <-req1Ch
   397  	require.NoError(t, err)
   398  	require.Equal(t, testBlock1, block1)
   399  }
   400  
   401  func TestBlockRetrievalWorkerStopIfFull(t *testing.T) {
   402  	ctx, cancel := context.WithTimeout(
   403  		context.Background(), individualTestTimeout)
   404  	defer cancel()
   405  	dbc, dbcConfig := initDiskBlockCacheTest(t)
   406  	defer dbc.Shutdown(ctx)
   407  
   408  	bg := newFakeBlockGetter(false)
   409  	q := newBlockRetrievalQueue(
   410  		1, 1, 0, newTestBlockRetrievalConfig(t, bg, dbc),
   411  		env.EmptyAppStateUpdater{})
   412  	require.NotNil(t, q)
   413  	<-q.TogglePrefetcher(false, nil, nil)
   414  	defer endBlockRetrievalQueueTest(t, q)
   415  
   416  	ptr := makeRandomBlockPointer(t)
   417  	syncCache := dbc.syncCache
   418  	workingCache := dbc.workingSetCache
   419  
   420  	t.Log("Set the cache maximum bytes to the current total.")
   421  	syncBytes, workingBytes := testGetDiskCacheBytes(syncCache, workingCache)
   422  	limiter := dbcConfig.DiskLimiter().(*backpressureDiskLimiter)
   423  	setLimiterLimits(limiter, syncBytes, workingBytes)
   424  
   425  	t.Log("Request with stop-if-full, when full")
   426  	testBlock := &data.FileBlock{}
   427  	req := q.Request(
   428  		ctx, 1, makeKMD(), ptr, testBlock, data.NoCacheEntry,
   429  		BlockRequestPrefetchUntilFull)
   430  	select {
   431  	case err := <-req:
   432  		require.IsType(t, DiskCacheTooFullForBlockError{}, err)
   433  	case <-ctx.Done():
   434  		require.FailNow(t, ctx.Err().Error())
   435  	}
   436  
   437  	t.Log("Request without stop-if-full, when full")
   438  	block := makeFakeFileBlock(t, false)
   439  	startCh, continueCh := bg.setBlockToReturn(ptr, block)
   440  	req = q.Request(
   441  		ctx, 1, makeKMD(), ptr, testBlock, data.NoCacheEntry,
   442  		BlockRequestSolo)
   443  	<-startCh
   444  	continueCh <- nil
   445  	select {
   446  	case err := <-req:
   447  		require.NoError(t, err)
   448  	case <-ctx.Done():
   449  		require.FailNow(t, ctx.Err().Error())
   450  	}
   451  }