* swarm/storage/localstore: most basic database * swarm/storage/localstore: fix typos and comments * swarm/shed: add uint64 field Dec and DecInBatch methods * swarm/storage/localstore: decrement size counter on ModeRemoval update * swarm/storage/localstore: unexport modeAccess and modeRemoval * swarm/storage/localstore: add WithRetrievalCompositeIndex * swarm/storage/localstore: add TestModeSyncing * swarm/storage/localstore: fix test name * swarm/storage/localstore: add TestModeUpload * swarm/storage/localstore: add TestModeRequest * swarm/storage/localstore: add TestModeSynced * swarm/storage/localstore: add TestModeAccess * swarm/storage/localstore: add TestModeRemoval * swarm/storage/localstore: add mock store option for chunk data * swarm/storage/localstore: add TestDB_pullIndex * swarm/storage/localstore: add TestDB_gcIndex * swarm/storage/localstore: change how batches are written * swarm/storage/localstore: add updateOnAccess function * swarm/storage/localhost: add DB.gcSize * swarm/storage/localstore: update comments * swarm/storage/localstore: add BenchmarkNew * swarm/storage/localstore: add retrieval tests benchmarks * swarm/storage/localstore: accessors redesign * swarm/storage/localstore: add semaphore for updateGC goroutine * swarm/storage/localstore: implement basic garbage collection * swarm/storage/localstore: optimize collectGarbage * swarm/storage/localstore: add more garbage collection tests cases * swarm/shed, swarm/storage/localstore: rename IndexItem to Item * swarm/shed: add Index.CountFrom * swarm/storage/localstore: persist gcSize * swarm/storage/localstore: remove composite retrieval index * swarm/shed: IterateWithPrefix and IterateWithPrefixFrom Index functions * swarm/storage/localstore: writeGCSize function with leveldb batch * swarm/storage/localstore: unexport modeSetRemove * swarm/storage/localstore: update writeGCSizeWorker comment * swarm/storage/localstore: add triggerGarbageCollection function * swarm/storage/localstore: call writeGCSize on DB Close * swarm/storage/localstore: additional comment in writeGCSizeWorker * swarm/storage/localstore: add MetricsPrefix option * swarm/storage/localstore: fix a typo * swamr/shed: only one Index Iterate function * swarm/storage/localstore: use shed Iterate function * swarm/shed: pass a new byte slice copy to index decode functions * swarm/storage/localstore: implement feed subscriptions * swarm/storage/localstore: add more subscriptions tests * swarm/storage/localsore: add parallel upload test * swarm/storage/localstore: use storage.MaxPO in subscription tests * swarm/storage/localstore: subscription of addresses instead chunks * swarm/storage/localstore: lock item address in collectGarbage iterator * swarm/storage/localstore: fix TestSubscribePull to include MaxPO * swarm/storage/localstore: improve subscriptions * swarm/storage/localstore: add TestDB_SubscribePull_sinceAndUntil test * swarm/storage/localstore: adjust pull sync tests * swarm/storage/localstore: remove writeGCSizeDelay and use literal * swarm/storage/localstore: adjust subscriptions tests delays and comments * swarm/storage/localstore: add godoc package overview * swarm/storage/localstore: fix a typo * swarm/storage/localstore: update package overview * swarm/storage/localstore: remove repeated index change * swarm/storage/localstore: rename ChunkInfo to ChunkDescriptor * swarm/storage/localstore: add comment in collectGarbageWorker * swarm/storage/localstore: replace atomics with mutexes for gcSize and tests * swarm/storage/localstore: protect addrs map in pull subs tests * swarm/storage/localstore: protect slices in push subs test * swarm/storage/localstore: protect chunks in TestModePutUpload_parallel * swarm/storage/localstore: fix a race in TestDB_updateGCSem defers * swarm/storage/localstore: remove parallel flag from tests * swarm/storage/localstore: fix a race in testDB_collectGarbageWorker * swarm/storage/localstore: remove unused code * swarm/storage/localstore: add more context to pull sub log messages * swarm/storage/localstore: BenchmarkPutUpload and global lock option * swarm/storage/localstore: pre-generate chunks in BenchmarkPutUpload * swarm/storage/localstore: correct useGlobalLock in collectGarbage * swarm/storage/localstore: fix typos and update comments * swarm/storage/localstore: update writeGCSize comment * swarm/storage/localstore: global batch write lock * swarm/storage/localstore: remove global lock option * swarm/storage/localstore: simplify DB.Close
		
			
				
	
	
		
			372 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2018 The go-ethereum Authors
 | |
| // This file is part of the go-ethereum library.
 | |
| //
 | |
| // The go-ethereum library is free software: you can redistribute it and/or modify
 | |
| // it under the terms of the GNU Lesser General Public License as published by
 | |
| // the Free Software Foundation, either version 3 of the License, or
 | |
| // (at your option) any later version.
 | |
| //
 | |
| // The go-ethereum library is distributed in the hope that it will be useful,
 | |
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | |
| // GNU Lesser General Public License for more details.
 | |
| //
 | |
| // You should have received a copy of the GNU Lesser General Public License
 | |
| // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
 | |
| 
 | |
| package localstore
 | |
| 
 | |
| import (
 | |
| 	"io/ioutil"
 | |
| 	"math/rand"
 | |
| 	"os"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/ethereum/go-ethereum/swarm/chunk"
 | |
| )
 | |
| 
 | |
| // TestDB_collectGarbageWorker tests garbage collection runs
 | |
| // by uploading and syncing a number of chunks.
 | |
| func TestDB_collectGarbageWorker(t *testing.T) {
 | |
| 	testDB_collectGarbageWorker(t)
 | |
| }
 | |
| 
 | |
| // TestDB_collectGarbageWorker_multipleBatches tests garbage
 | |
| // collection runs by uploading and syncing a number of
 | |
| // chunks by having multiple smaller batches.
 | |
| func TestDB_collectGarbageWorker_multipleBatches(t *testing.T) {
 | |
| 	// lower the maximal number of chunks in a single
 | |
| 	// gc batch to ensure multiple batches.
 | |
| 	defer func(s uint64) { gcBatchSize = s }(gcBatchSize)
 | |
| 	gcBatchSize = 2
 | |
| 
 | |
| 	testDB_collectGarbageWorker(t)
 | |
| }
 | |
| 
 | |
| // testDB_collectGarbageWorker is a helper test function to test
 | |
| // garbage collection runs by uploading and syncing a number of chunks.
 | |
| func testDB_collectGarbageWorker(t *testing.T) {
 | |
| 	t.Helper()
 | |
| 
 | |
| 	chunkCount := 150
 | |
| 
 | |
| 	db, cleanupFunc := newTestDB(t, &Options{
 | |
| 		Capacity: 100,
 | |
| 	})
 | |
| 	testHookCollectGarbageChan := make(chan uint64)
 | |
| 	defer setTestHookCollectGarbage(func(collectedCount uint64) {
 | |
| 		select {
 | |
| 		case testHookCollectGarbageChan <- collectedCount:
 | |
| 		case <-db.close:
 | |
| 		}
 | |
| 	})()
 | |
| 	defer cleanupFunc()
 | |
| 
 | |
| 	uploader := db.NewPutter(ModePutUpload)
 | |
| 	syncer := db.NewSetter(ModeSetSync)
 | |
| 
 | |
| 	addrs := make([]chunk.Address, 0)
 | |
| 
 | |
| 	// upload random chunks
 | |
| 	for i := 0; i < chunkCount; i++ {
 | |
| 		chunk := generateTestRandomChunk()
 | |
| 
 | |
| 		err := uploader.Put(chunk)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		err = syncer.Set(chunk.Address())
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		addrs = append(addrs, chunk.Address())
 | |
| 	}
 | |
| 
 | |
| 	gcTarget := db.gcTarget()
 | |
| 
 | |
| 	for {
 | |
| 		select {
 | |
| 		case <-testHookCollectGarbageChan:
 | |
| 		case <-time.After(10 * time.Second):
 | |
| 			t.Error("collect garbage timeout")
 | |
| 		}
 | |
| 		gcSize, err := db.gcSize.Get()
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		if gcSize == gcTarget {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	t.Run("pull index count", newItemsCountTest(db.pullIndex, int(gcTarget)))
 | |
| 
 | |
| 	t.Run("gc index count", newItemsCountTest(db.gcIndex, int(gcTarget)))
 | |
| 
 | |
| 	t.Run("gc size", newIndexGCSizeTest(db))
 | |
| 
 | |
| 	// the first synced chunk should be removed
 | |
| 	t.Run("get the first synced chunk", func(t *testing.T) {
 | |
| 		_, err := db.NewGetter(ModeGetRequest).Get(addrs[0])
 | |
| 		if err != chunk.ErrChunkNotFound {
 | |
| 			t.Errorf("got error %v, want %v", err, chunk.ErrChunkNotFound)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	// last synced chunk should not be removed
 | |
| 	t.Run("get most recent synced chunk", func(t *testing.T) {
 | |
| 		_, err := db.NewGetter(ModeGetRequest).Get(addrs[len(addrs)-1])
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // TestDB_collectGarbageWorker_withRequests is a helper test function
 | |
| // to test garbage collection runs by uploading, syncing and
 | |
| // requesting a number of chunks.
 | |
| func TestDB_collectGarbageWorker_withRequests(t *testing.T) {
 | |
| 	db, cleanupFunc := newTestDB(t, &Options{
 | |
| 		Capacity: 100,
 | |
| 	})
 | |
| 	defer cleanupFunc()
 | |
| 
 | |
| 	uploader := db.NewPutter(ModePutUpload)
 | |
| 	syncer := db.NewSetter(ModeSetSync)
 | |
| 
 | |
| 	testHookCollectGarbageChan := make(chan uint64)
 | |
| 	defer setTestHookCollectGarbage(func(collectedCount uint64) {
 | |
| 		testHookCollectGarbageChan <- collectedCount
 | |
| 	})()
 | |
| 
 | |
| 	addrs := make([]chunk.Address, 0)
 | |
| 
 | |
| 	// upload random chunks just up to the capacity
 | |
| 	for i := 0; i < int(db.capacity)-1; i++ {
 | |
| 		chunk := generateTestRandomChunk()
 | |
| 
 | |
| 		err := uploader.Put(chunk)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		err = syncer.Set(chunk.Address())
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		addrs = append(addrs, chunk.Address())
 | |
| 	}
 | |
| 
 | |
| 	// set update gc test hook to signal when
 | |
| 	// update gc goroutine is done by closing
 | |
| 	// testHookUpdateGCChan channel
 | |
| 	testHookUpdateGCChan := make(chan struct{})
 | |
| 	resetTestHookUpdateGC := setTestHookUpdateGC(func() {
 | |
| 		close(testHookUpdateGCChan)
 | |
| 	})
 | |
| 
 | |
| 	// request the latest synced chunk
 | |
| 	// to prioritize it in the gc index
 | |
| 	// not to be collected
 | |
| 	_, err := db.NewGetter(ModeGetRequest).Get(addrs[0])
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	// wait for update gc goroutine to finish for garbage
 | |
| 	// collector to be correctly triggered after the last upload
 | |
| 	select {
 | |
| 	case <-testHookUpdateGCChan:
 | |
| 	case <-time.After(10 * time.Second):
 | |
| 		t.Fatal("updateGC was not called after getting chunk with ModeGetRequest")
 | |
| 	}
 | |
| 
 | |
| 	// no need to wait for update gc hook anymore
 | |
| 	resetTestHookUpdateGC()
 | |
| 
 | |
| 	// upload and sync another chunk to trigger
 | |
| 	// garbage collection
 | |
| 	ch := generateTestRandomChunk()
 | |
| 	err = uploader.Put(ch)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	err = syncer.Set(ch.Address())
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	addrs = append(addrs, ch.Address())
 | |
| 
 | |
| 	// wait for garbage collection
 | |
| 
 | |
| 	gcTarget := db.gcTarget()
 | |
| 
 | |
| 	var totalCollectedCount uint64
 | |
| 	for {
 | |
| 		select {
 | |
| 		case c := <-testHookCollectGarbageChan:
 | |
| 			totalCollectedCount += c
 | |
| 		case <-time.After(10 * time.Second):
 | |
| 			t.Error("collect garbage timeout")
 | |
| 		}
 | |
| 		gcSize, err := db.gcSize.Get()
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 		if gcSize == gcTarget {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	wantTotalCollectedCount := uint64(len(addrs)) - gcTarget
 | |
| 	if totalCollectedCount != wantTotalCollectedCount {
 | |
| 		t.Errorf("total collected chunks %v, want %v", totalCollectedCount, wantTotalCollectedCount)
 | |
| 	}
 | |
| 
 | |
| 	t.Run("pull index count", newItemsCountTest(db.pullIndex, int(gcTarget)))
 | |
| 
 | |
| 	t.Run("gc index count", newItemsCountTest(db.gcIndex, int(gcTarget)))
 | |
| 
 | |
| 	t.Run("gc size", newIndexGCSizeTest(db))
 | |
| 
 | |
| 	// requested chunk should not be removed
 | |
| 	t.Run("get requested chunk", func(t *testing.T) {
 | |
| 		_, err := db.NewGetter(ModeGetRequest).Get(addrs[0])
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	// the second synced chunk should be removed
 | |
| 	t.Run("get gc-ed chunk", func(t *testing.T) {
 | |
| 		_, err := db.NewGetter(ModeGetRequest).Get(addrs[1])
 | |
| 		if err != chunk.ErrChunkNotFound {
 | |
| 			t.Errorf("got error %v, want %v", err, chunk.ErrChunkNotFound)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	// last synced chunk should not be removed
 | |
| 	t.Run("get most recent synced chunk", func(t *testing.T) {
 | |
| 		_, err := db.NewGetter(ModeGetRequest).Get(addrs[len(addrs)-1])
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // TestDB_gcSize checks if gcSize has a correct value after
 | |
| // database is initialized with existing data.
 | |
| func TestDB_gcSize(t *testing.T) {
 | |
| 	dir, err := ioutil.TempDir("", "localstore-stored-gc-size")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer os.RemoveAll(dir)
 | |
| 	baseKey := make([]byte, 32)
 | |
| 	if _, err := rand.Read(baseKey); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	db, err := New(dir, baseKey, nil)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	uploader := db.NewPutter(ModePutUpload)
 | |
| 	syncer := db.NewSetter(ModeSetSync)
 | |
| 
 | |
| 	count := 100
 | |
| 
 | |
| 	for i := 0; i < count; i++ {
 | |
| 		chunk := generateTestRandomChunk()
 | |
| 
 | |
| 		err := uploader.Put(chunk)
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 
 | |
| 		err = syncer.Set(chunk.Address())
 | |
| 		if err != nil {
 | |
| 			t.Fatal(err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if err := db.Close(); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	db, err = New(dir, baseKey, nil)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	defer db.Close()
 | |
| 
 | |
| 	t.Run("gc index size", newIndexGCSizeTest(db))
 | |
| }
 | |
| 
 | |
| // setTestHookCollectGarbage sets testHookCollectGarbage and
 | |
| // returns a function that will reset it to the
 | |
| // value before the change.
 | |
| func setTestHookCollectGarbage(h func(collectedCount uint64)) (reset func()) {
 | |
| 	current := testHookCollectGarbage
 | |
| 	reset = func() { testHookCollectGarbage = current }
 | |
| 	testHookCollectGarbage = h
 | |
| 	return reset
 | |
| }
 | |
| 
 | |
| // TestSetTestHookCollectGarbage tests if setTestHookCollectGarbage changes
 | |
| // testHookCollectGarbage function correctly and if its reset function
 | |
| // resets the original function.
 | |
| func TestSetTestHookCollectGarbage(t *testing.T) {
 | |
| 	// Set the current function after the test finishes.
 | |
| 	defer func(h func(collectedCount uint64)) { testHookCollectGarbage = h }(testHookCollectGarbage)
 | |
| 
 | |
| 	// expected value for the unchanged function
 | |
| 	original := 1
 | |
| 	// expected value for the changed function
 | |
| 	changed := 2
 | |
| 
 | |
| 	// this variable will be set with two different functions
 | |
| 	var got int
 | |
| 
 | |
| 	// define the original (unchanged) functions
 | |
| 	testHookCollectGarbage = func(_ uint64) {
 | |
| 		got = original
 | |
| 	}
 | |
| 
 | |
| 	// set got variable
 | |
| 	testHookCollectGarbage(0)
 | |
| 
 | |
| 	// test if got variable is set correctly
 | |
| 	if got != original {
 | |
| 		t.Errorf("got hook value %v, want %v", got, original)
 | |
| 	}
 | |
| 
 | |
| 	// set the new function
 | |
| 	reset := setTestHookCollectGarbage(func(_ uint64) {
 | |
| 		got = changed
 | |
| 	})
 | |
| 
 | |
| 	// set got variable
 | |
| 	testHookCollectGarbage(0)
 | |
| 
 | |
| 	// test if got variable is set correctly to changed value
 | |
| 	if got != changed {
 | |
| 		t.Errorf("got hook value %v, want %v", got, changed)
 | |
| 	}
 | |
| 
 | |
| 	// set the function to the original one
 | |
| 	reset()
 | |
| 
 | |
| 	// set got variable
 | |
| 	testHookCollectGarbage(0)
 | |
| 
 | |
| 	// test if got variable is set correctly to original value
 | |
| 	if got != original {
 | |
| 		t.Errorf("got hook value %v, want %v", got, original)
 | |
| 	}
 | |
| }
 |