github.com/susy-go/susy-graviton@v0.0.0-20190614130430-36cddae42305/swarm/storage/localstore/localstore_test.go (about)

     1  // Copyleft 2018 The susy-graviton Authors
     2  // This file is part of the susy-graviton library.
     3  //
     4  // The susy-graviton library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The susy-graviton library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MSRCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the susy-graviton library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package localstore
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io/ioutil"
    23  	"math/rand"
    24  	"os"
    25  	"sort"
    26  	"strconv"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	ch "github.com/susy-go/susy-graviton/swarm/chunk"
    32  	"github.com/susy-go/susy-graviton/swarm/shed"
    33  	"github.com/susy-go/susy-graviton/swarm/storage"
    34  	"github.com/syndtr/goleveldb/leveldb"
    35  )
    36  
    37  // TestDB validates if the chunk can be uploaded and
    38  // correctly retrieved.
    39  func TestDB(t *testing.T) {
    40  	db, cleanupFunc := newTestDB(t, nil)
    41  	defer cleanupFunc()
    42  
    43  	chunk := generateRandomChunk()
    44  
    45  	err := db.NewPutter(ModePutUpload).Put(chunk)
    46  	if err != nil {
    47  		t.Fatal(err)
    48  	}
    49  
    50  	got, err := db.NewGetter(ModeGetRequest).Get(chunk.Address())
    51  	if err != nil {
    52  		t.Fatal(err)
    53  	}
    54  
    55  	if !bytes.Equal(got.Address(), chunk.Address()) {
    56  		t.Errorf("got address %x, want %x", got.Address(), chunk.Address())
    57  	}
    58  	if !bytes.Equal(got.Data(), chunk.Data()) {
    59  		t.Errorf("got data %x, want %x", got.Data(), chunk.Data())
    60  	}
    61  }
    62  
    63  // TestDB_updateGCSem tests maxParallelUpdateGC limit.
    64  // This test temporary sets the limit to a low number,
    65  // makes updateGC function execution time longer by
    66  // setting a custom testHookUpdateGC function with a sleep
    67  // and a count current and maximal number of goroutines.
    68  func TestDB_updateGCSem(t *testing.T) {
    69  	updateGCSleep := time.Second
    70  	var count int
    71  	var max int
    72  	var mu sync.Mutex
    73  	defer setTestHookUpdateGC(func() {
    74  		mu.Lock()
    75  		// add to the count of current goroutines
    76  		count++
    77  		if count > max {
    78  			// set maximal detected numbers of goroutines
    79  			max = count
    80  		}
    81  		mu.Unlock()
    82  
    83  		// wait for some time to ensure multiple parallel goroutines
    84  		time.Sleep(updateGCSleep)
    85  
    86  		mu.Lock()
    87  		count--
    88  		mu.Unlock()
    89  	})()
    90  
    91  	defer func(m int) { maxParallelUpdateGC = m }(maxParallelUpdateGC)
    92  	maxParallelUpdateGC = 3
    93  
    94  	db, cleanupFunc := newTestDB(t, nil)
    95  	defer cleanupFunc()
    96  
    97  	chunk := generateRandomChunk()
    98  
    99  	err := db.NewPutter(ModePutUpload).Put(chunk)
   100  	if err != nil {
   101  		t.Fatal(err)
   102  	}
   103  
   104  	getter := db.NewGetter(ModeGetRequest)
   105  
   106  	// get more chunks then maxParallelUpdateGC
   107  	// in time shorter then updateGCSleep
   108  	for i := 0; i < 5; i++ {
   109  		_, err = getter.Get(chunk.Address())
   110  		if err != nil {
   111  			t.Fatal(err)
   112  		}
   113  	}
   114  
   115  	if max != maxParallelUpdateGC {
   116  		t.Errorf("got max %v, want %v", max, maxParallelUpdateGC)
   117  	}
   118  }
   119  
   120  // BenchmarkNew measures the time that New function
   121  // needs to initialize and count the number of key/value
   122  // pairs in GC index.
   123  // This benchmark generates a number of chunks, uploads them,
   124  // sets them to synced state for them to enter the GC index,
   125  // and measures the execution time of New function by creating
   126  // new databases with the same data directory.
   127  //
   128  // This benchmark takes significant amount of time.
   129  //
   130  // Measurements on MacBook Pro (Retina, 15-inch, Mid 2014) show
   131  // that New function executes around 1s for database with 1M chunks.
   132  //
   133  // # go test -benchmem -run=none github.com/susy-go/susy-graviton/swarm/storage/localstore -bench BenchmarkNew -v -timeout 20m
   134  // goos: darwin
   135  // goarch: amd64
   136  // pkg: github.com/susy-go/susy-graviton/swarm/storage/localstore
   137  // BenchmarkNew/1000-8         	     200	  11672414 ns/op	 9570960 B/op	   10008 allocs/op
   138  // BenchmarkNew/10000-8        	     100	  14890609 ns/op	10490118 B/op	    7759 allocs/op
   139  // BenchmarkNew/100000-8       	      20	  58334080 ns/op	17763157 B/op	   22978 allocs/op
   140  // BenchmarkNew/1000000-8      	       2	 748595153 ns/op	45297404 B/op	  253242 allocs/op
   141  // PASS
   142  func BenchmarkNew(b *testing.B) {
   143  	if testing.Short() {
   144  		b.Skip("skipping benchmark in short mode")
   145  	}
   146  	for _, count := range []int{
   147  		1000,
   148  		10000,
   149  		100000,
   150  		1000000,
   151  	} {
   152  		b.Run(strconv.Itoa(count), func(b *testing.B) {
   153  			dir, err := ioutil.TempDir("", "localstore-new-benchmark")
   154  			if err != nil {
   155  				b.Fatal(err)
   156  			}
   157  			defer os.RemoveAll(dir)
   158  			baseKey := make([]byte, 32)
   159  			if _, err := rand.Read(baseKey); err != nil {
   160  				b.Fatal(err)
   161  			}
   162  			db, err := New(dir, baseKey, nil)
   163  			if err != nil {
   164  				b.Fatal(err)
   165  			}
   166  			uploader := db.NewPutter(ModePutUpload)
   167  			syncer := db.NewSetter(ModeSetSync)
   168  			for i := 0; i < count; i++ {
   169  				chunk := generateFakeRandomChunk()
   170  				err := uploader.Put(chunk)
   171  				if err != nil {
   172  					b.Fatal(err)
   173  				}
   174  				err = syncer.Set(chunk.Address())
   175  				if err != nil {
   176  					b.Fatal(err)
   177  				}
   178  			}
   179  			err = db.Close()
   180  			if err != nil {
   181  				b.Fatal(err)
   182  			}
   183  			b.ResetTimer()
   184  
   185  			for n := 0; n < b.N; n++ {
   186  				b.StartTimer()
   187  				db, err := New(dir, baseKey, nil)
   188  				b.StopTimer()
   189  
   190  				if err != nil {
   191  					b.Fatal(err)
   192  				}
   193  				err = db.Close()
   194  				if err != nil {
   195  					b.Fatal(err)
   196  				}
   197  			}
   198  		})
   199  	}
   200  }
   201  
   202  // newTestDB is a helper function that constructs a
   203  // temporary database and returns a cleanup function that must
   204  // be called to remove the data.
   205  func newTestDB(t testing.TB, o *Options) (db *DB, cleanupFunc func()) {
   206  	t.Helper()
   207  
   208  	dir, err := ioutil.TempDir("", "localstore-test")
   209  	if err != nil {
   210  		t.Fatal(err)
   211  	}
   212  	cleanupFunc = func() { os.RemoveAll(dir) }
   213  	baseKey := make([]byte, 32)
   214  	if _, err := rand.Read(baseKey); err != nil {
   215  		t.Fatal(err)
   216  	}
   217  	db, err = New(dir, baseKey, o)
   218  	if err != nil {
   219  		cleanupFunc()
   220  		t.Fatal(err)
   221  	}
   222  	cleanupFunc = func() {
   223  		err := db.Close()
   224  		if err != nil {
   225  			t.Error(err)
   226  		}
   227  		os.RemoveAll(dir)
   228  	}
   229  	return db, cleanupFunc
   230  }
   231  
   232  // generateRandomChunk generates a valid Chunk with
   233  // data size of default chunk size.
   234  func generateRandomChunk() storage.Chunk {
   235  	return storage.GenerateRandomChunk(ch.DefaultSize)
   236  }
   237  
   238  func init() {
   239  	// needed for generateFakeRandomChunk
   240  	rand.Seed(time.Now().UnixNano())
   241  }
   242  
   243  // generateFakeRandomChunk generates a Chunk that is not
   244  // valid, but it contains a random key and a random value.
   245  // This function is faster then storage.GenerateRandomChunk
   246  // which generates a valid chunk.
   247  // Some tests in this package do not need valid chunks, just
   248  // random data, and their execution time can be decreased
   249  // using this function.
   250  func generateFakeRandomChunk() storage.Chunk {
   251  	data := make([]byte, ch.DefaultSize)
   252  	rand.Read(data)
   253  	key := make([]byte, 32)
   254  	rand.Read(key)
   255  	return storage.NewChunk(key, data)
   256  }
   257  
   258  // TestGenerateFakeRandomChunk validates that
   259  // generateFakeRandomChunk returns random data by comparing
   260  // two generated chunks.
   261  func TestGenerateFakeRandomChunk(t *testing.T) {
   262  	c1 := generateFakeRandomChunk()
   263  	c2 := generateFakeRandomChunk()
   264  	addrLen := len(c1.Address())
   265  	if addrLen != 32 {
   266  		t.Errorf("first chunk address length %v, want %v", addrLen, 32)
   267  	}
   268  	dataLen := len(c1.Data())
   269  	if dataLen != ch.DefaultSize {
   270  		t.Errorf("first chunk data length %v, want %v", dataLen, ch.DefaultSize)
   271  	}
   272  	addrLen = len(c2.Address())
   273  	if addrLen != 32 {
   274  		t.Errorf("second chunk address length %v, want %v", addrLen, 32)
   275  	}
   276  	dataLen = len(c2.Data())
   277  	if dataLen != ch.DefaultSize {
   278  		t.Errorf("second chunk data length %v, want %v", dataLen, ch.DefaultSize)
   279  	}
   280  	if bytes.Equal(c1.Address(), c2.Address()) {
   281  		t.Error("fake chunks addresses do not differ")
   282  	}
   283  	if bytes.Equal(c1.Data(), c2.Data()) {
   284  		t.Error("fake chunks data bytes do not differ")
   285  	}
   286  }
   287  
   288  // newRetrieveIndexesTest returns a test function that validates if the right
   289  // chunk values are in the retrieval indexes.
   290  func newRetrieveIndexesTest(db *DB, chunk storage.Chunk, storeTimestamp, accessTimestamp int64) func(t *testing.T) {
   291  	return func(t *testing.T) {
   292  		item, err := db.retrievalDataIndex.Get(addressToItem(chunk.Address()))
   293  		if err != nil {
   294  			t.Fatal(err)
   295  		}
   296  		validateItem(t, item, chunk.Address(), chunk.Data(), storeTimestamp, 0)
   297  
   298  		// access index should not be set
   299  		wantErr := leveldb.ErrNotFound
   300  		item, err = db.retrievalAccessIndex.Get(addressToItem(chunk.Address()))
   301  		if err != wantErr {
   302  			t.Errorf("got error %v, want %v", err, wantErr)
   303  		}
   304  	}
   305  }
   306  
   307  // newRetrieveIndexesTestWithAccess returns a test function that validates if the right
   308  // chunk values are in the retrieval indexes when access time must be stored.
   309  func newRetrieveIndexesTestWithAccess(db *DB, chunk storage.Chunk, storeTimestamp, accessTimestamp int64) func(t *testing.T) {
   310  	return func(t *testing.T) {
   311  		item, err := db.retrievalDataIndex.Get(addressToItem(chunk.Address()))
   312  		if err != nil {
   313  			t.Fatal(err)
   314  		}
   315  		validateItem(t, item, chunk.Address(), chunk.Data(), storeTimestamp, 0)
   316  
   317  		if accessTimestamp > 0 {
   318  			item, err = db.retrievalAccessIndex.Get(addressToItem(chunk.Address()))
   319  			if err != nil {
   320  				t.Fatal(err)
   321  			}
   322  			validateItem(t, item, chunk.Address(), nil, 0, accessTimestamp)
   323  		}
   324  	}
   325  }
   326  
   327  // newPullIndexTest returns a test function that validates if the right
   328  // chunk values are in the pull index.
   329  func newPullIndexTest(db *DB, chunk storage.Chunk, storeTimestamp int64, wantError error) func(t *testing.T) {
   330  	return func(t *testing.T) {
   331  		item, err := db.pullIndex.Get(shed.Item{
   332  			Address:        chunk.Address(),
   333  			StoreTimestamp: storeTimestamp,
   334  		})
   335  		if err != wantError {
   336  			t.Errorf("got error %v, want %v", err, wantError)
   337  		}
   338  		if err == nil {
   339  			validateItem(t, item, chunk.Address(), nil, storeTimestamp, 0)
   340  		}
   341  	}
   342  }
   343  
   344  // newPushIndexTest returns a test function that validates if the right
   345  // chunk values are in the push index.
   346  func newPushIndexTest(db *DB, chunk storage.Chunk, storeTimestamp int64, wantError error) func(t *testing.T) {
   347  	return func(t *testing.T) {
   348  		item, err := db.pushIndex.Get(shed.Item{
   349  			Address:        chunk.Address(),
   350  			StoreTimestamp: storeTimestamp,
   351  		})
   352  		if err != wantError {
   353  			t.Errorf("got error %v, want %v", err, wantError)
   354  		}
   355  		if err == nil {
   356  			validateItem(t, item, chunk.Address(), nil, storeTimestamp, 0)
   357  		}
   358  	}
   359  }
   360  
   361  // newGCIndexTest returns a test function that validates if the right
   362  // chunk values are in the push index.
   363  func newGCIndexTest(db *DB, chunk storage.Chunk, storeTimestamp, accessTimestamp int64) func(t *testing.T) {
   364  	return func(t *testing.T) {
   365  		item, err := db.gcIndex.Get(shed.Item{
   366  			Address:         chunk.Address(),
   367  			StoreTimestamp:  storeTimestamp,
   368  			AccessTimestamp: accessTimestamp,
   369  		})
   370  		if err != nil {
   371  			t.Fatal(err)
   372  		}
   373  		validateItem(t, item, chunk.Address(), nil, storeTimestamp, accessTimestamp)
   374  	}
   375  }
   376  
   377  // newItemsCountTest returns a test function that validates if
   378  // an index contains expected number of key/value pairs.
   379  func newItemsCountTest(i shed.Index, want int) func(t *testing.T) {
   380  	return func(t *testing.T) {
   381  		var c int
   382  		err := i.Iterate(func(item shed.Item) (stop bool, err error) {
   383  			c++
   384  			return
   385  		}, nil)
   386  		if err != nil {
   387  			t.Fatal(err)
   388  		}
   389  		if c != want {
   390  			t.Errorf("got %v items in index, want %v", c, want)
   391  		}
   392  	}
   393  }
   394  
   395  // newIndexGCSizeTest retruns a test function that validates if DB.gcSize
   396  // value is the same as the number of items in DB.gcIndex.
   397  func newIndexGCSizeTest(db *DB) func(t *testing.T) {
   398  	return func(t *testing.T) {
   399  		var want int64
   400  		err := db.gcIndex.Iterate(func(item shed.Item) (stop bool, err error) {
   401  			want++
   402  			return
   403  		}, nil)
   404  		if err != nil {
   405  			t.Fatal(err)
   406  		}
   407  		got := db.getGCSize()
   408  		if got != want {
   409  			t.Errorf("got gc size %v, want %v", got, want)
   410  		}
   411  	}
   412  }
   413  
   414  // testIndexChunk embeds storageChunk with additional data that is stored
   415  // in database. It is used for index values validations.
   416  type testIndexChunk struct {
   417  	storage.Chunk
   418  	storeTimestamp int64
   419  }
   420  
   421  // testItemsOrder tests the order of chunks in the index. If sortFunc is not nil,
   422  // chunks will be sorted with it before validation.
   423  func testItemsOrder(t *testing.T, i shed.Index, chunks []testIndexChunk, sortFunc func(i, j int) (less bool)) {
   424  	newItemsCountTest(i, len(chunks))(t)
   425  
   426  	if sortFunc != nil {
   427  		sort.Slice(chunks, sortFunc)
   428  	}
   429  
   430  	var cursor int
   431  	err := i.Iterate(func(item shed.Item) (stop bool, err error) {
   432  		want := chunks[cursor].Address()
   433  		got := item.Address
   434  		if !bytes.Equal(got, want) {
   435  			return true, fmt.Errorf("got address %x at position %v, want %x", got, cursor, want)
   436  		}
   437  		cursor++
   438  		return false, nil
   439  	}, nil)
   440  	if err != nil {
   441  		t.Fatal(err)
   442  	}
   443  }
   444  
   445  // validateItem is a helper function that checks Item values.
   446  func validateItem(t *testing.T, item shed.Item, address, data []byte, storeTimestamp, accessTimestamp int64) {
   447  	t.Helper()
   448  
   449  	if !bytes.Equal(item.Address, address) {
   450  		t.Errorf("got item address %x, want %x", item.Address, address)
   451  	}
   452  	if !bytes.Equal(item.Data, data) {
   453  		t.Errorf("got item data %x, want %x", item.Data, data)
   454  	}
   455  	if item.StoreTimestamp != storeTimestamp {
   456  		t.Errorf("got item store timestamp %v, want %v", item.StoreTimestamp, storeTimestamp)
   457  	}
   458  	if item.AccessTimestamp != accessTimestamp {
   459  		t.Errorf("got item access timestamp %v, want %v", item.AccessTimestamp, accessTimestamp)
   460  	}
   461  }
   462  
   463  // setNow replaces now function and
   464  // returns a function that will reset it to the
   465  // value before the change.
   466  func setNow(f func() int64) (reset func()) {
   467  	current := now
   468  	reset = func() { now = current }
   469  	now = f
   470  	return reset
   471  }
   472  
   473  // TestSetNow tests if setNow function changes now function
   474  // correctly and if its reset function resets the original function.
   475  func TestSetNow(t *testing.T) {
   476  	// set the current function after the test finishes
   477  	defer func(f func() int64) { now = f }(now)
   478  
   479  	// expected value for the unchanged function
   480  	var original int64 = 1
   481  	// expected value for the changed function
   482  	var changed int64 = 2
   483  
   484  	// define the original (unchanged) functions
   485  	now = func() int64 {
   486  		return original
   487  	}
   488  
   489  	// get the time
   490  	got := now()
   491  
   492  	// test if got variable is set correctly
   493  	if got != original {
   494  		t.Errorf("got now value %v, want %v", got, original)
   495  	}
   496  
   497  	// set the new function
   498  	reset := setNow(func() int64 {
   499  		return changed
   500  	})
   501  
   502  	// get the time
   503  	got = now()
   504  
   505  	// test if got variable is set correctly to changed value
   506  	if got != changed {
   507  		t.Errorf("got hook value %v, want %v", got, changed)
   508  	}
   509  
   510  	// set the function to the original one
   511  	reset()
   512  
   513  	// get the time
   514  	got = now()
   515  
   516  	// test if got variable is set correctly to original value
   517  	if got != original {
   518  		t.Errorf("got hook value %v, want %v", got, original)
   519  	}
   520  }