github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/eds/cache/accessor_cache_test.go (about)

     1  package cache
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"io"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/filecoin-project/dagstore"
    13  	"github.com/filecoin-project/dagstore/shard"
    14  	blocks "github.com/ipfs/go-block-format"
    15  	"github.com/ipfs/go-cid"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  func TestAccessorCache(t *testing.T) {
    20  	t.Run("add / get item from cache", func(t *testing.T) {
    21  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    22  		defer cancel()
    23  		cache, err := NewAccessorCache("test", 1)
    24  		require.NoError(t, err)
    25  
    26  		// add accessor to the cache
    27  		key := shard.KeyFromString("key")
    28  		mock := &mockAccessor{
    29  			data: []byte("test_data"),
    30  		}
    31  		loaded, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
    32  			return mock, nil
    33  		})
    34  		require.NoError(t, err)
    35  
    36  		// check if item exists
    37  		got, err := cache.Get(key)
    38  		require.NoError(t, err)
    39  
    40  		l, err := io.ReadAll(loaded.Reader())
    41  		require.NoError(t, err)
    42  		require.Equal(t, mock.data, l)
    43  		g, err := io.ReadAll(got.Reader())
    44  		require.NoError(t, err)
    45  		require.Equal(t, mock.data, g)
    46  	})
    47  
    48  	t.Run("get blockstore from accessor", func(t *testing.T) {
    49  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    50  		defer cancel()
    51  		cache, err := NewAccessorCache("test", 1)
    52  		require.NoError(t, err)
    53  
    54  		// add accessor to the cache
    55  		key := shard.KeyFromString("key")
    56  		mock := &mockAccessor{}
    57  		accessor, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
    58  			return mock, nil
    59  		})
    60  		require.NoError(t, err)
    61  
    62  		// check if item exists
    63  		_, err = cache.Get(key)
    64  		require.NoError(t, err)
    65  
    66  		// blockstore should be created only after first request
    67  		require.Equal(t, 0, mock.returnedBs)
    68  
    69  		// try to get blockstore
    70  		_, err = accessor.Blockstore()
    71  		require.NoError(t, err)
    72  
    73  		// second call to blockstore should return same blockstore
    74  		_, err = accessor.Blockstore()
    75  		require.NoError(t, err)
    76  		require.Equal(t, 1, mock.returnedBs)
    77  	})
    78  
    79  	t.Run("remove an item", func(t *testing.T) {
    80  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    81  		defer cancel()
    82  		cache, err := NewAccessorCache("test", 1)
    83  		require.NoError(t, err)
    84  
    85  		// add accessor to the cache
    86  		key := shard.KeyFromString("key")
    87  		mock := &mockAccessor{}
    88  		ac, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
    89  			return mock, nil
    90  		})
    91  		require.NoError(t, err)
    92  		err = ac.Close()
    93  		require.NoError(t, err)
    94  
    95  		err = cache.Remove(key)
    96  		require.NoError(t, err)
    97  
    98  		// accessor should be closed on removal
    99  		mock.checkClosed(t, true)
   100  
   101  		// check if item exists
   102  		_, err = cache.Get(key)
   103  		require.ErrorIs(t, err, errCacheMiss)
   104  	})
   105  
   106  	t.Run("successive reads should read the same data", func(t *testing.T) {
   107  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   108  		defer cancel()
   109  		cache, err := NewAccessorCache("test", 1)
   110  		require.NoError(t, err)
   111  
   112  		// add accessor to the cache
   113  		key := shard.KeyFromString("key")
   114  		mock := &mockAccessor{data: []byte("test")}
   115  		accessor, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
   116  			return mock, nil
   117  		})
   118  		require.NoError(t, err)
   119  
   120  		loaded, err := io.ReadAll(accessor.Reader())
   121  		require.NoError(t, err)
   122  		require.Equal(t, mock.data, loaded)
   123  
   124  		for i := 0; i < 2; i++ {
   125  			accessor, err = cache.Get(key)
   126  			require.NoError(t, err)
   127  			got, err := io.ReadAll(accessor.Reader())
   128  			require.NoError(t, err)
   129  			require.Equal(t, mock.data, got)
   130  		}
   131  	})
   132  
   133  	t.Run("removed by eviction", func(t *testing.T) {
   134  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   135  		defer cancel()
   136  		cache, err := NewAccessorCache("test", 1)
   137  		require.NoError(t, err)
   138  
   139  		// add accessor to the cache
   140  		key := shard.KeyFromString("key")
   141  		mock := &mockAccessor{}
   142  		ac1, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
   143  			return mock, nil
   144  		})
   145  		require.NoError(t, err)
   146  		err = ac1.Close()
   147  		require.NoError(t, err)
   148  
   149  		// add second item
   150  		key2 := shard.KeyFromString("key2")
   151  		ac2, err := cache.GetOrLoad(ctx, key2, func(ctx context.Context, key shard.Key) (Accessor, error) {
   152  			return mock, nil
   153  		})
   154  		require.NoError(t, err)
   155  		err = ac2.Close()
   156  		require.NoError(t, err)
   157  
   158  		// accessor should be closed on removal by eviction
   159  		mock.checkClosed(t, true)
   160  
   161  		// check if item evicted
   162  		_, err = cache.Get(key)
   163  		require.ErrorIs(t, err, errCacheMiss)
   164  	})
   165  
   166  	t.Run("close on accessor is not closing underlying accessor", func(t *testing.T) {
   167  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   168  		defer cancel()
   169  		cache, err := NewAccessorCache("test", 1)
   170  		require.NoError(t, err)
   171  
   172  		// add accessor to the cache
   173  		key := shard.KeyFromString("key")
   174  		mock := &mockAccessor{}
   175  		_, err = cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
   176  			return mock, nil
   177  		})
   178  		require.NoError(t, err)
   179  
   180  		// check if item exists
   181  		accessor, err := cache.Get(key)
   182  		require.NoError(t, err)
   183  		require.NotNil(t, accessor)
   184  
   185  		// close on returned accessor should not close inner accessor
   186  		err = accessor.Close()
   187  		require.NoError(t, err)
   188  
   189  		// check that close was not performed on inner accessor
   190  		mock.checkClosed(t, false)
   191  	})
   192  
   193  	t.Run("close on accessor should wait all readers to finish", func(t *testing.T) {
   194  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   195  		defer cancel()
   196  		cache, err := NewAccessorCache("test", 1)
   197  		require.NoError(t, err)
   198  
   199  		// add accessor to the cache
   200  		key := shard.KeyFromString("key")
   201  		mock := &mockAccessor{}
   202  		accessor1, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) {
   203  			return mock, nil
   204  		})
   205  		require.NoError(t, err)
   206  
   207  		// create second readers
   208  		accessor2, err := cache.Get(key)
   209  		require.NoError(t, err)
   210  
   211  		// initialize close
   212  		done := make(chan struct{})
   213  		go func() {
   214  			err := cache.Remove(key)
   215  			require.NoError(t, err)
   216  			close(done)
   217  		}()
   218  
   219  		// close on first reader and check that it is not enough to release the inner accessor
   220  		err = accessor1.Close()
   221  		require.NoError(t, err)
   222  		mock.checkClosed(t, false)
   223  
   224  		// second close from same reader should not release accessor either
   225  		err = accessor1.Close()
   226  		require.NoError(t, err)
   227  		mock.checkClosed(t, false)
   228  
   229  		// reads for item that is being evicted should result in errCacheMiss
   230  		_, err = cache.Get(key)
   231  		require.ErrorIs(t, err, errCacheMiss)
   232  
   233  		// close second reader and wait for accessor to be closed
   234  		err = accessor2.Close()
   235  		require.NoError(t, err)
   236  		// wait until close is performed on accessor
   237  		select {
   238  		case <-done:
   239  		case <-ctx.Done():
   240  			t.Fatal("timeout reached")
   241  		}
   242  
   243  		// item will be removed
   244  		mock.checkClosed(t, true)
   245  	})
   246  
   247  	t.Run("slow reader should not block eviction", func(t *testing.T) {
   248  		ctx, cancel := context.WithTimeout(context.Background(), time.Second)
   249  		defer cancel()
   250  		cache, err := NewAccessorCache("test", 1)
   251  		require.NoError(t, err)
   252  
   253  		// add accessor to the cache
   254  		key1 := shard.KeyFromString("key1")
   255  		mock1 := &mockAccessor{}
   256  		accessor1, err := cache.GetOrLoad(ctx, key1, func(ctx context.Context, key shard.Key) (Accessor, error) {
   257  			return mock1, nil
   258  		})
   259  		require.NoError(t, err)
   260  
   261  		// add second accessor, to trigger eviction of the first one
   262  		key2 := shard.KeyFromString("key2")
   263  		mock2 := &mockAccessor{}
   264  		accessor2, err := cache.GetOrLoad(ctx, key2, func(ctx context.Context, key shard.Key) (Accessor, error) {
   265  			return mock2, nil
   266  		})
   267  		require.NoError(t, err)
   268  
   269  		// first accessor should be evicted from cache
   270  		_, err = cache.Get(key1)
   271  		require.ErrorIs(t, err, errCacheMiss)
   272  
   273  		// first accessor should not be closed before all refs are released by Close() is calls.
   274  		mock1.checkClosed(t, false)
   275  
   276  		// after Close() is called on first accessor, it is free to get closed
   277  		err = accessor1.Close()
   278  		require.NoError(t, err)
   279  		mock1.checkClosed(t, true)
   280  
   281  		// after Close called on second accessor, it should stay in cache (not closed)
   282  		err = accessor2.Close()
   283  		require.NoError(t, err)
   284  		mock2.checkClosed(t, false)
   285  	})
   286  }
   287  
   288  type mockAccessor struct {
   289  	m          sync.Mutex
   290  	data       []byte
   291  	isClosed   bool
   292  	returnedBs int
   293  }
   294  
   295  func (m *mockAccessor) Reader() io.Reader {
   296  	m.m.Lock()
   297  	defer m.m.Unlock()
   298  	return bytes.NewBuffer(m.data)
   299  }
   300  
   301  func (m *mockAccessor) Blockstore() (dagstore.ReadBlockstore, error) {
   302  	m.m.Lock()
   303  	defer m.m.Unlock()
   304  	if m.returnedBs > 0 {
   305  		return nil, errors.New("blockstore already returned")
   306  	}
   307  	m.returnedBs++
   308  	return rbsMock{}, nil
   309  }
   310  
   311  func (m *mockAccessor) Close() error {
   312  	m.m.Lock()
   313  	defer m.m.Unlock()
   314  	if m.isClosed {
   315  		return errors.New("already closed")
   316  	}
   317  	m.isClosed = true
   318  	return nil
   319  }
   320  
   321  func (m *mockAccessor) checkClosed(t *testing.T, expected bool) {
   322  	// item will be removed in background, so give it some time to settle
   323  	time.Sleep(time.Millisecond * 100)
   324  	m.m.Lock()
   325  	defer m.m.Unlock()
   326  	require.Equal(t, expected, m.isClosed)
   327  }
   328  
   329  // rbsMock is a dagstore.ReadBlockstore mock
   330  type rbsMock struct{}
   331  
   332  func (r rbsMock) Has(context.Context, cid.Cid) (bool, error) {
   333  	panic("implement me")
   334  }
   335  
   336  func (r rbsMock) Get(_ context.Context, _ cid.Cid) (blocks.Block, error) {
   337  	panic("implement me")
   338  }
   339  
   340  func (r rbsMock) GetSize(context.Context, cid.Cid) (int, error) {
   341  	panic("implement me")
   342  }
   343  
   344  func (r rbsMock) AllKeysChan(context.Context) (<-chan cid.Cid, error) {
   345  	panic("implement me")
   346  }
   347  
   348  func (r rbsMock) HashOnRead(bool) {
   349  	panic("implement me")
   350  }