github.com/ethersphere/bee/v2@v2.2.0/pkg/storer/internal/chunkstore/chunkstore_test.go (about)

     1  // Copyright 2022 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package chunkstore_test
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io/fs"
    12  	"math"
    13  	"os"
    14  	"testing"
    15  
    16  	"github.com/ethersphere/bee/v2/pkg/sharky"
    17  	"github.com/ethersphere/bee/v2/pkg/storer/internal/transaction"
    18  
    19  	"github.com/ethersphere/bee/v2/pkg/storage"
    20  	"github.com/ethersphere/bee/v2/pkg/storage/inmemstore"
    21  	"github.com/ethersphere/bee/v2/pkg/storage/storagetest"
    22  	chunktest "github.com/ethersphere/bee/v2/pkg/storage/testing"
    23  	"github.com/ethersphere/bee/v2/pkg/storer/internal/chunkstore"
    24  	"github.com/ethersphere/bee/v2/pkg/swarm"
    25  	"github.com/spf13/afero"
    26  	"github.com/stretchr/testify/assert"
    27  )
    28  
    29  func TestRetrievalIndexItem(t *testing.T) {
    30  	t.Parallel()
    31  
    32  	tests := []struct {
    33  		name string
    34  		test *storagetest.ItemMarshalAndUnmarshalTest
    35  	}{{
    36  		name: "zero values",
    37  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    38  			Item:       &chunkstore.RetrievalIndexItem{},
    39  			Factory:    func() storage.Item { return new(chunkstore.RetrievalIndexItem) },
    40  			MarshalErr: chunkstore.ErrMarshalInvalidRetrievalIndexItemAddress,
    41  		},
    42  	}, {
    43  		name: "zero address",
    44  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    45  			Item: &chunkstore.RetrievalIndexItem{
    46  				Address: swarm.ZeroAddress,
    47  			},
    48  			Factory:    func() storage.Item { return new(chunkstore.RetrievalIndexItem) },
    49  			MarshalErr: chunkstore.ErrMarshalInvalidRetrievalIndexItemAddress,
    50  		},
    51  	}, {
    52  		name: "min values",
    53  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    54  			Item: &chunkstore.RetrievalIndexItem{
    55  				Address: swarm.NewAddress(storagetest.MinAddressBytes[:]),
    56  			},
    57  			Factory: func() storage.Item { return new(chunkstore.RetrievalIndexItem) },
    58  		},
    59  	}, {
    60  		name: "max values",
    61  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    62  			Item: &chunkstore.RetrievalIndexItem{
    63  				Address:   swarm.NewAddress(storagetest.MaxAddressBytes[:]),
    64  				Timestamp: math.MaxUint64,
    65  				Location: sharky.Location{
    66  					Shard:  math.MaxUint8,
    67  					Slot:   math.MaxUint32,
    68  					Length: math.MaxUint16,
    69  				},
    70  				RefCnt: math.MaxUint8,
    71  			},
    72  			Factory: func() storage.Item { return new(chunkstore.RetrievalIndexItem) },
    73  		},
    74  	}, {
    75  		name: "invalid size",
    76  		test: &storagetest.ItemMarshalAndUnmarshalTest{
    77  			Item: &storagetest.ItemStub{
    78  				MarshalBuf:   []byte{0xFF},
    79  				UnmarshalBuf: []byte{0xFF},
    80  			},
    81  			Factory:      func() storage.Item { return new(chunkstore.RetrievalIndexItem) },
    82  			UnmarshalErr: chunkstore.ErrUnmarshalInvalidRetrievalIndexItemSize,
    83  		},
    84  	}}
    85  
    86  	for _, tc := range tests {
    87  		tc := tc
    88  
    89  		t.Run(fmt.Sprintf("%s marshal/unmarshal", tc.name), func(t *testing.T) {
    90  			t.Parallel()
    91  
    92  			storagetest.TestItemMarshalAndUnmarshal(t, tc.test)
    93  		})
    94  
    95  		t.Run(fmt.Sprintf("%s clone", tc.name), func(t *testing.T) {
    96  			t.Parallel()
    97  
    98  			storagetest.TestItemClone(t, &storagetest.ItemCloneTest{
    99  				Item:    tc.test.Item,
   100  				CmpOpts: tc.test.CmpOpts,
   101  			})
   102  		})
   103  	}
   104  }
   105  
   106  type memFS struct {
   107  	afero.Fs
   108  }
   109  
   110  func (m *memFS) Open(path string) (fs.File, error) {
   111  	return m.Fs.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
   112  }
   113  
   114  func TestChunkStore(t *testing.T) {
   115  	t.Parallel()
   116  
   117  	store := inmemstore.New()
   118  	sharky, err := sharky.New(&memFS{Fs: afero.NewMemMapFs()}, 1, swarm.SocMaxChunkSize)
   119  	if err != nil {
   120  		t.Fatal(err)
   121  	}
   122  
   123  	st := transaction.NewStorage(sharky, store)
   124  
   125  	t.Cleanup(func() {
   126  		if err := store.Close(); err != nil {
   127  			t.Errorf("inmem store close failed: %v", err)
   128  		}
   129  	})
   130  
   131  	testChunks := chunktest.GenerateTestRandomChunks(50)
   132  
   133  	t.Run("put chunks", func(t *testing.T) {
   134  		for _, ch := range testChunks {
   135  			err := st.Run(context.Background(), func(s transaction.Store) error {
   136  				return s.ChunkStore().Put(context.TODO(), ch)
   137  			})
   138  			if err != nil {
   139  				t.Fatalf("failed putting new chunk: %v", err)
   140  			}
   141  		}
   142  	})
   143  
   144  	t.Run("put existing chunks", func(t *testing.T) {
   145  		for idx, ch := range testChunks {
   146  			// only put duplicates for odd numbered indexes
   147  			if idx%2 != 0 {
   148  				err := st.Run(context.Background(), func(s transaction.Store) error {
   149  					return s.ChunkStore().Put(context.TODO(), ch)
   150  				})
   151  				if err != nil {
   152  					t.Fatalf("failed putting new chunk: %v", err)
   153  				}
   154  			}
   155  		}
   156  	})
   157  
   158  	t.Run("get chunks", func(t *testing.T) {
   159  		for _, ch := range testChunks {
   160  			readCh, err := st.ChunkStore().Get(context.TODO(), ch.Address())
   161  			if err != nil {
   162  				t.Fatalf("failed getting chunk: %v", err)
   163  			}
   164  			if !readCh.Equal(ch) {
   165  				t.Fatal("read chunk doesn't match")
   166  			}
   167  		}
   168  	})
   169  
   170  	t.Run("has chunks", func(t *testing.T) {
   171  		for _, ch := range testChunks {
   172  			exists, err := st.ChunkStore().Has(context.TODO(), ch.Address())
   173  			if err != nil {
   174  				t.Fatalf("failed getting chunk: %v", err)
   175  			}
   176  			if !exists {
   177  				t.Fatalf("chunk not found: %s", ch.Address())
   178  			}
   179  		}
   180  	})
   181  
   182  	t.Run("iterate chunks", func(t *testing.T) {
   183  		count := 0
   184  		err := chunkstore.Iterate(context.TODO(), store, sharky, func(_ swarm.Chunk) (bool, error) {
   185  			count++
   186  			return false, nil
   187  		})
   188  		if err != nil {
   189  			t.Fatalf("unexpected error while iteration: %v", err)
   190  		}
   191  		if count != 50 {
   192  			t.Fatalf("unexpected no of chunks, exp: %d, found: %d", 50, count)
   193  		}
   194  	})
   195  
   196  	t.Run("iterate chunk entries", func(t *testing.T) {
   197  		count, shared := 0, 0
   198  		err := chunkstore.IterateChunkEntries(store, func(_ swarm.Address, cnt uint32) (bool, error) {
   199  			count++
   200  			if cnt > 1 {
   201  				shared++
   202  			}
   203  			return false, nil
   204  		})
   205  		if err != nil {
   206  			t.Fatalf("unexpected error while iteration: %v", err)
   207  		}
   208  		if count != 50 {
   209  			t.Fatalf("unexpected no of chunks, exp: %d, found: %d", 50, count)
   210  		}
   211  		if shared != 25 {
   212  			t.Fatalf("unexpected no of chunks, exp: %d, found: %d", 25, shared)
   213  		}
   214  	})
   215  
   216  	t.Run("delete unique chunks", func(t *testing.T) {
   217  		for idx, ch := range testChunks {
   218  			// Delete all even numbered indexes along with 0
   219  			if idx%2 == 0 {
   220  				err := st.Run(context.Background(), func(s transaction.Store) error {
   221  					return s.ChunkStore().Delete(context.TODO(), ch.Address())
   222  				})
   223  				if err != nil {
   224  					t.Fatalf("failed deleting chunk: %v", err)
   225  				}
   226  			}
   227  		}
   228  	})
   229  
   230  	t.Run("check deleted chunks", func(t *testing.T) {
   231  		for idx, ch := range testChunks {
   232  			if idx%2 == 0 {
   233  				// Check even numbered indexes are deleted
   234  				_, err := st.ChunkStore().Get(context.TODO(), ch.Address())
   235  				if !errors.Is(err, storage.ErrNotFound) {
   236  					t.Fatalf("expected storage not found error found: %v", err)
   237  				}
   238  				found, err := st.ChunkStore().Has(context.TODO(), ch.Address())
   239  				if err != nil {
   240  					t.Fatalf("unexpected error in Has: %v", err)
   241  				}
   242  				if found {
   243  					t.Fatal("expected chunk to not be found")
   244  				}
   245  			} else {
   246  				// Check rest of the entries are intact
   247  				readCh, err := st.ChunkStore().Get(context.TODO(), ch.Address())
   248  				if err != nil {
   249  					t.Fatalf("failed getting chunk: %v", err)
   250  				}
   251  				if !readCh.Equal(ch) {
   252  					t.Fatal("read chunk doesn't match")
   253  				}
   254  				exists, err := st.ChunkStore().Has(context.TODO(), ch.Address())
   255  				if err != nil {
   256  					t.Fatalf("failed getting chunk: %v", err)
   257  				}
   258  				if !exists {
   259  					t.Fatalf("chunk not found: %s", ch.Address())
   260  				}
   261  			}
   262  		}
   263  	})
   264  
   265  	t.Run("iterate chunks after delete", func(t *testing.T) {
   266  		count := 0
   267  		err := chunkstore.Iterate(context.TODO(), store, sharky, func(_ swarm.Chunk) (bool, error) {
   268  			count++
   269  			return false, nil
   270  		})
   271  		if err != nil {
   272  			t.Fatalf("unexpected error while iteration: %v", err)
   273  		}
   274  		if count != 25 {
   275  			t.Fatalf("unexpected no of chunks, exp: %d, found: %d", 25, count)
   276  		}
   277  	})
   278  
   279  	// due to refCnt 2, these chunks should be present in the store even after delete
   280  	t.Run("delete duplicate chunks", func(t *testing.T) {
   281  		for idx, ch := range testChunks {
   282  			if idx%2 != 0 {
   283  				err := st.Run(context.Background(), func(s transaction.Store) error {
   284  					return s.ChunkStore().Delete(context.TODO(), ch.Address())
   285  				})
   286  				if err != nil {
   287  					t.Fatalf("failed deleting chunk: %v", err)
   288  				}
   289  			}
   290  		}
   291  	})
   292  
   293  	t.Run("check chunks still exists", func(t *testing.T) {
   294  		for idx, ch := range testChunks {
   295  			if idx%2 != 0 {
   296  				readCh, err := st.ChunkStore().Get(context.TODO(), ch.Address())
   297  				if err != nil {
   298  					t.Fatalf("failed getting chunk: %v", err)
   299  				}
   300  				if !readCh.Equal(ch) {
   301  					t.Fatal("read chunk doesn't match")
   302  				}
   303  				exists, err := st.ChunkStore().Has(context.TODO(), ch.Address())
   304  				if err != nil {
   305  					t.Fatalf("failed getting chunk: %v", err)
   306  				}
   307  				if !exists {
   308  					t.Fatalf("chunk not found: %s", ch.Address())
   309  				}
   310  			}
   311  		}
   312  	})
   313  
   314  	t.Run("delete duplicate chunks again", func(t *testing.T) {
   315  		for idx, ch := range testChunks {
   316  			if idx%2 != 0 {
   317  				err := st.Run(context.Background(), func(s transaction.Store) error {
   318  					return s.ChunkStore().Delete(context.TODO(), ch.Address())
   319  				})
   320  				if err != nil {
   321  					t.Fatalf("failed deleting chunk: %v", err)
   322  				}
   323  			}
   324  		}
   325  	})
   326  
   327  	t.Run("check all are deleted", func(t *testing.T) {
   328  		count := 0
   329  		err := chunkstore.Iterate(context.TODO(), store, sharky, func(_ swarm.Chunk) (bool, error) {
   330  			count++
   331  			return false, nil
   332  		})
   333  		if err != nil {
   334  			t.Fatalf("unexpected error while iteration: %v", err)
   335  		}
   336  		if count != 0 {
   337  			t.Fatalf("unexpected no of chunks, exp: %d, found: %d", 0, count)
   338  		}
   339  	})
   340  
   341  	t.Run("close store", func(t *testing.T) {
   342  		err := st.Close()
   343  		if err != nil {
   344  			t.Fatalf("unexpected error during close: %v", err)
   345  		}
   346  	})
   347  }
   348  
   349  // TestIterateLocations asserts that all stored chunks
   350  // are retrievable by sharky using IterateLocations.
   351  func TestIterateLocations(t *testing.T) {
   352  	t.Parallel()
   353  
   354  	const chunksCount = 50
   355  
   356  	st := makeStorage(t)
   357  	testChunks := chunktest.GenerateTestRandomChunks(chunksCount)
   358  	ctx := context.Background()
   359  
   360  	for _, ch := range testChunks {
   361  		assert.NoError(t, st.Run(context.Background(), func(s transaction.Store) error { return s.ChunkStore().Put(ctx, ch) }))
   362  	}
   363  
   364  	readCount := 0
   365  	respC := chunkstore.IterateLocations(ctx, st.IndexStore())
   366  
   367  	for resp := range respC {
   368  		assert.NoError(t, resp.Err)
   369  
   370  		buf := make([]byte, resp.Location.Length)
   371  		assert.NoError(t, st.sharky.Read(ctx, resp.Location, buf))
   372  
   373  		assert.True(t, swarm.ContainsChunkWithData(testChunks, buf))
   374  		readCount++
   375  	}
   376  
   377  	assert.Equal(t, chunksCount, readCount)
   378  }
   379  
   380  // TestIterateLocations_Stop asserts that IterateLocations will
   381  // stop iteration when context is canceled.
   382  func TestIterateLocations_Stop(t *testing.T) {
   383  	t.Parallel()
   384  
   385  	const chunksCount = 50
   386  	const stopReadAt = 10
   387  
   388  	st := makeStorage(t)
   389  	testChunks := chunktest.GenerateTestRandomChunks(chunksCount)
   390  	ctx, cancel := context.WithCancel(context.Background())
   391  	defer cancel()
   392  
   393  	for _, ch := range testChunks {
   394  		assert.NoError(t, st.Run(context.Background(), func(s transaction.Store) error { return s.ChunkStore().Put(ctx, ch) }))
   395  	}
   396  
   397  	readCount := 0
   398  	respC := chunkstore.IterateLocations(ctx, st.IndexStore())
   399  
   400  	for resp := range respC {
   401  		if resp.Err != nil {
   402  			assert.ErrorIs(t, resp.Err, context.Canceled)
   403  			break
   404  		}
   405  
   406  		buf := make([]byte, resp.Location.Length)
   407  		if err := st.sharky.Read(ctx, resp.Location, buf); err != nil {
   408  			assert.ErrorIs(t, err, context.Canceled)
   409  			break
   410  		}
   411  
   412  		assert.True(t, swarm.ContainsChunkWithData(testChunks, buf))
   413  		readCount++
   414  
   415  		if readCount == stopReadAt {
   416  			cancel()
   417  		}
   418  	}
   419  
   420  	assert.InDelta(t, stopReadAt, readCount, 1)
   421  }
   422  
   423  type chunkStore struct {
   424  	transaction.Storage
   425  	sharky *sharky.Store
   426  }
   427  
   428  func makeStorage(t *testing.T) *chunkStore {
   429  	t.Helper()
   430  
   431  	store := inmemstore.New()
   432  	sharky, err := sharky.New(&memFS{Fs: afero.NewMemMapFs()}, 1, swarm.SocMaxChunkSize)
   433  	assert.NoError(t, err)
   434  
   435  	t.Cleanup(func() {
   436  		assert.NoError(t, store.Close())
   437  		assert.NoError(t, sharky.Close())
   438  	})
   439  
   440  	return &chunkStore{transaction.NewStorage(sharky, store), sharky}
   441  }