add support for block pruning via ABCI Commit response (#4588)

* Added BlockStore.DeleteBlock()

* Added initial block pruner prototype

* wip

* Added BlockStore.PruneBlocks()

* Added consensus setting for block pruning

* Added BlockStore base

* Error on replay if base does not have blocks

* Handle missing blocks when sending VoteSetMaj23Message

* Error message tweak

* Properly update blockstore state

* Error message fix again

* blockchain: ignore peer missing blocks

* Added FIXME

* Added test for block replay with truncated history

* Handle peer base in blockchain reactor

* Improved replay error handling

* Added tests for Store.PruneBlocks()

* Fix non-RPC handling of truncated block history

* Panic on missing block meta in needProofBlock()

* Updated changelog

* Handle truncated block history in RPC layer

* Added info about earliest block in /status RPC

* Reorder height and base in blockchain reactor messages

* Updated changelog

* Fix tests

* Appease linter

* Minor review fixes

* Non-empty BlockStores should always have base > 0

* Update code to assume base > 0 invariant

* Added blockstore tests for pruning to 0

* Make sure we don't prune below the current base

* Added BlockStore.Size()

* config: added retain_blocks recommendations

* Update v1 blockchain reactor to handle blockstore base

* Added state database pruning

* Propagate errors on missing validator sets

* Comment tweaks

* Improved error message

Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>

* use ABCI field ResponseCommit.retain_height instead of retain-blocks config option

* remove State.RetainHeight, return value instead

* fix minor issues

* rename pruneHeights() to pruneBlocks()

* noop to fix GitHub borkage

Co-authored-by: Anton Kaliaev <anton.kalyaev@gmail.com>
This commit is contained in:
Erik Grinaker
2020-04-03 10:38:32 +02:00
committed by GitHub
parent ce50dda66c
commit 4298bbcc4e
42 changed files with 1208 additions and 393 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/pkg/errors"
db "github.com/tendermint/tm-db"
dbm "github.com/tendermint/tm-db"
"github.com/tendermint/tendermint/types"
@@ -24,6 +25,8 @@ Currently the precommit signatures are duplicated in the Block parts as
well as the Commit. In the future this may change, perhaps by moving
the Commit data outside the Block. (TODO)
The store can be assumed to contain all contiguous blocks between base and height (inclusive).
// NOTE: BlockStore methods will panic if they encounter errors
// deserializing loaded data, indicating probable corruption on disk.
*/
@@ -31,6 +34,7 @@ type BlockStore struct {
db dbm.DB
mtx sync.RWMutex
base int64
height int64
}
@@ -39,18 +43,36 @@ type BlockStore struct {
func NewBlockStore(db dbm.DB) *BlockStore {
bsjson := LoadBlockStoreStateJSON(db)
return &BlockStore{
base: bsjson.Base,
height: bsjson.Height,
db: db,
}
}
// Height returns the last known contiguous block height.
// Base returns the first known contiguous block height, or 0 for empty block stores.
func (bs *BlockStore) Base() int64 {
bs.mtx.RLock()
defer bs.mtx.RUnlock()
return bs.base
}
// Height returns the last known contiguous block height, or 0 for empty block stores.
func (bs *BlockStore) Height() int64 {
bs.mtx.RLock()
defer bs.mtx.RUnlock()
return bs.height
}
// Size returns the number of blocks in the block store.
func (bs *BlockStore) Size() int64 {
bs.mtx.RLock()
defer bs.mtx.RUnlock()
if bs.height == 0 {
return 0
}
return bs.height - bs.base + 1
}
// LoadBlock returns the block with the given height.
// If no block is found for that height, it returns nil.
func (bs *BlockStore) LoadBlock(height int64) *types.Block {
@@ -171,6 +193,74 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit {
return commit
}
// PruneBlocks removes block up to (but not including) a height. It returns number of blocks pruned.
func (bs *BlockStore) PruneBlocks(height int64) (uint64, error) {
if height <= 0 {
return 0, fmt.Errorf("height must be greater than 0")
}
bs.mtx.RLock()
if height > bs.height {
bs.mtx.RUnlock()
return 0, fmt.Errorf("cannot prune beyond the latest height %v", bs.height)
}
base := bs.base
bs.mtx.RUnlock()
if height < base {
return 0, fmt.Errorf("cannot prune to height %v, it is lower than base height %v",
height, base)
}
pruned := uint64(0)
batch := bs.db.NewBatch()
defer batch.Close()
flush := func(batch db.Batch, base int64) error {
// We can't trust batches to be atomic, so update base first to make sure noone
// tries to access missing blocks.
bs.mtx.Lock()
bs.base = base
bs.mtx.Unlock()
bs.saveState()
err := batch.WriteSync()
if err != nil {
return fmt.Errorf("failed to prune up to height %v: %w", base, err)
}
batch.Close()
return nil
}
for h := base; h < height; h++ {
meta := bs.LoadBlockMeta(h)
if meta == nil { // assume already deleted
continue
}
batch.Delete(calcBlockMetaKey(h))
batch.Delete(calcBlockHashKey(meta.BlockID.Hash))
batch.Delete(calcBlockCommitKey(h))
batch.Delete(calcSeenCommitKey(h))
for p := 0; p < meta.BlockID.PartsHeader.Total; p++ {
batch.Delete(calcBlockPartKey(h, p))
}
pruned++
// flush every 1000 blocks to avoid batches becoming too large
if pruned%1000 == 0 && pruned > 0 {
err := flush(batch, h)
if err != nil {
return 0, err
}
batch = bs.db.NewBatch()
defer batch.Close()
}
}
err := flush(batch, height)
if err != nil {
return 0, err
}
return pruned, nil
}
// SaveBlock persists the given block, blockParts, and seenCommit to the underlying db.
// blockParts: Must be parts of the block
// seenCommit: The +2/3 precommits that were seen which committed at height.
@@ -213,14 +303,17 @@ func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, s
seenCommitBytes := cdc.MustMarshalBinaryBare(seenCommit)
bs.db.Set(calcSeenCommitKey(height), seenCommitBytes)
// Save new BlockStoreStateJSON descriptor
BlockStoreStateJSON{Height: height}.Save(bs.db)
// Done!
bs.mtx.Lock()
bs.height = height
if bs.base == 0 && height == 1 {
bs.base = 1
}
bs.mtx.Unlock()
// Save new BlockStoreStateJSON descriptor
bs.saveState()
// Flush
bs.db.SetSync(nil, nil)
}
@@ -233,6 +326,16 @@ func (bs *BlockStore) saveBlockPart(height int64, index int, part *types.Part) {
bs.db.Set(calcBlockPartKey(height, index), partBytes)
}
func (bs *BlockStore) saveState() {
bs.mtx.RLock()
bsJSON := BlockStoreStateJSON{
Base: bs.base,
Height: bs.height,
}
bs.mtx.RUnlock()
bsJSON.Save(bs.db)
}
//-----------------------------------------------------------------------------
func calcBlockMetaKey(height int64) []byte {
@@ -261,6 +364,7 @@ var blockStoreKey = []byte("blockStore")
// BlockStoreStateJSON is the block store state JSON structure.
type BlockStoreStateJSON struct {
Base int64 `json:"base"`
Height int64 `json:"height"`
}
@@ -282,6 +386,7 @@ func LoadBlockStoreStateJSON(db dbm.DB) BlockStoreStateJSON {
}
if len(bytes) == 0 {
return BlockStoreStateJSON{
Base: 0,
Height: 0,
}
}
@@ -290,5 +395,9 @@ func LoadBlockStoreStateJSON(db dbm.DB) BlockStoreStateJSON {
if err != nil {
panic(fmt.Sprintf("Could not unmarshal bytes: %X", bytes))
}
// Backwards compatibility with persisted data from before Base existed.
if bsj.Height > 0 && bsj.Base == 0 {
bsj.Base = 1
}
return bsj
}

View File

@@ -65,20 +65,39 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu
func TestLoadBlockStoreStateJSON(t *testing.T) {
db := db.NewMemDB()
bsj := &BlockStoreStateJSON{Base: 100, Height: 1000}
bsj.Save(db)
retrBSJ := LoadBlockStoreStateJSON(db)
assert.Equal(t, *bsj, retrBSJ, "expected the retrieved DBs to match")
}
func TestLoadBlockStoreStateJSON_Empty(t *testing.T) {
db := db.NewMemDB()
bsj := &BlockStoreStateJSON{}
bsj.Save(db)
retrBSJ := LoadBlockStoreStateJSON(db)
assert.Equal(t, BlockStoreStateJSON{}, retrBSJ, "expected the retrieved DBs to match")
}
func TestLoadBlockStoreStateJSON_NoBase(t *testing.T) {
db := db.NewMemDB()
bsj := &BlockStoreStateJSON{Height: 1000}
bsj.Save(db)
retrBSJ := LoadBlockStoreStateJSON(db)
assert.Equal(t, *bsj, retrBSJ, "expected the retrieved DBs to match")
assert.Equal(t, BlockStoreStateJSON{Base: 1, Height: 1000}, retrBSJ, "expected the retrieved DBs to match")
}
func TestNewBlockStore(t *testing.T) {
db := db.NewMemDB()
err := db.Set(blockStoreKey, []byte(`{"height": "10000"}`))
err := db.Set(blockStoreKey, []byte(`{"base": "100", "height": "10000"}`))
require.NoError(t, err)
bs := NewBlockStore(db)
require.Equal(t, int64(100), bs.Base(), "failed to properly parse blockstore")
require.Equal(t, int64(10000), bs.Height(), "failed to properly parse blockstore")
panicCausers := []struct {
@@ -140,6 +159,7 @@ func TestMain(m *testing.M) {
func TestBlockStoreSaveLoadBlock(t *testing.T) {
state, bs, cleanup := makeStateAndBlockStore(log.NewTMLogger(new(bytes.Buffer)))
defer cleanup()
require.Equal(t, bs.Base(), int64(0), "initially the base should be zero")
require.Equal(t, bs.Height(), int64(0), "initially the height should be zero")
// check there are no blocks at various heights
@@ -155,7 +175,8 @@ func TestBlockStoreSaveLoadBlock(t *testing.T) {
validPartSet := block.MakePartSet(2)
seenCommit := makeTestCommit(10, tmtime.Now())
bs.SaveBlock(block, partSet, seenCommit)
require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed")
require.EqualValues(t, 1, bs.Base(), "expecting the new height to be changed")
require.EqualValues(t, block.Header.Height, bs.Height(), "expecting the new height to be changed")
incompletePartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 2})
uncontiguousPartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 0})
@@ -364,6 +385,92 @@ func TestLoadBlockPart(t *testing.T) {
"expecting successful retrieval of previously saved block")
}
func TestPruneBlocks(t *testing.T) {
config := cfg.ResetTestRoot("blockchain_reactor_test")
defer os.RemoveAll(config.RootDir)
state, err := sm.LoadStateFromDBOrGenesisFile(dbm.NewMemDB(), config.GenesisFile())
require.NoError(t, err)
db := dbm.NewMemDB()
bs := NewBlockStore(db)
assert.EqualValues(t, 0, bs.Base())
assert.EqualValues(t, 0, bs.Height())
assert.EqualValues(t, 0, bs.Size())
// pruning an empty store should error, even when pruning to 0
_, err = bs.PruneBlocks(1)
require.Error(t, err)
_, err = bs.PruneBlocks(0)
require.Error(t, err)
// make more than 1000 blocks, to test batch deletions
for h := int64(1); h <= 1500; h++ {
block := makeBlock(h, state, new(types.Commit))
partSet := block.MakePartSet(2)
seenCommit := makeTestCommit(h, tmtime.Now())
bs.SaveBlock(block, partSet, seenCommit)
}
assert.EqualValues(t, 1, bs.Base())
assert.EqualValues(t, 1500, bs.Height())
assert.EqualValues(t, 1500, bs.Size())
prunedBlock := bs.LoadBlock(1199)
// Check that basic pruning works
pruned, err := bs.PruneBlocks(1200)
require.NoError(t, err)
assert.EqualValues(t, 1199, pruned)
assert.EqualValues(t, 1200, bs.Base())
assert.EqualValues(t, 1500, bs.Height())
assert.EqualValues(t, 301, bs.Size())
assert.EqualValues(t, BlockStoreStateJSON{
Base: 1200,
Height: 1500,
}, LoadBlockStoreStateJSON(db))
require.NotNil(t, bs.LoadBlock(1200))
require.Nil(t, bs.LoadBlock(1199))
require.Nil(t, bs.LoadBlockByHash(prunedBlock.Hash()))
require.Nil(t, bs.LoadBlockCommit(1199))
require.Nil(t, bs.LoadBlockMeta(1199))
require.Nil(t, bs.LoadBlockPart(1199, 1))
for i := int64(1); i < 1200; i++ {
require.Nil(t, bs.LoadBlock(i))
}
for i := int64(1200); i <= 1500; i++ {
require.NotNil(t, bs.LoadBlock(i))
}
// Pruning below the current base should error
_, err = bs.PruneBlocks(1199)
require.Error(t, err)
// Pruning to the current base should work
pruned, err = bs.PruneBlocks(1200)
require.NoError(t, err)
assert.EqualValues(t, 0, pruned)
// Pruning again should work
pruned, err = bs.PruneBlocks(1300)
require.NoError(t, err)
assert.EqualValues(t, 100, pruned)
assert.EqualValues(t, 1300, bs.Base())
// Pruning beyond the current height should error
_, err = bs.PruneBlocks(1501)
require.Error(t, err)
// Pruning to the current height should work
pruned, err = bs.PruneBlocks(1500)
require.NoError(t, err)
assert.EqualValues(t, 200, pruned)
assert.Nil(t, bs.LoadBlock(1499))
assert.NotNil(t, bs.LoadBlock(1500))
assert.Nil(t, bs.LoadBlock(1501))
}
func TestLoadBlockMeta(t *testing.T) {
bs, db := freshBlockStore()
height := int64(10)