erigon-pulse/state/merge.go
ledgerwatch 157b4299e4
[erigon2] Continuation on domains and inverted indices, putting things together (#476)
* Add scan files tests, create new aggregator type

* Fix lint

* windows test fix

* Add delelte test

* AggCollation

* More functions to Aggregator

* More aggregator functions

* Update

* More functions

* More functions

Co-authored-by: Alexey Sharp <alexeysharp@Alexeys-iMac.local>
Co-authored-by: Alex Sharp <alexsharp@Alexs-MacBook-Pro.local>
2022-06-02 21:40:58 +01:00

568 lines
19 KiB
Go

/*
Copyright 2022 Erigon contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package state
import (
"bytes"
"container/heap"
"context"
"fmt"
"os"
"path/filepath"
"github.com/google/btree"
"github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon-lib/compress"
"github.com/ledgerwatch/erigon-lib/recsplit"
"github.com/ledgerwatch/erigon-lib/recsplit/eliasfano32"
"github.com/ledgerwatch/log/v3"
)
func (d *Domain) endTxNumMinimax() uint64 {
var minimax uint64
for fType := FileType(0); fType < NumberOfTypes; fType++ {
if d.files[fType].Len() > 0 {
endTxNum := d.files[fType].Max().(*filesItem).endTxNum
if minimax == 0 || endTxNum < minimax {
minimax = endTxNum
}
}
}
return minimax
}
func (ii *InvertedIndex) endTxNumMinimax() uint64 {
var minimax uint64
if ii.files.Len() > 0 {
endTxNum := ii.files.Max().(*filesItem).endTxNum
if minimax == 0 || endTxNum < minimax {
minimax = endTxNum
}
}
return minimax
}
// findMergeRange assumes that all fTypes in d.files have items at least as far as maxEndTxNum
// That is why only Values type is inspected
func (d *Domain) findMergeRange(maxEndTxNum, maxSpan uint64) (bool, uint64, uint64) {
var minFound bool
var startTxNum, endTxNum uint64
d.files[Values].Ascend(func(i btree.Item) bool {
item := i.(*filesItem)
if item.endTxNum > maxEndTxNum {
return false
}
endStep := item.endTxNum / d.aggregationStep
spanStep := endStep & -endStep // Extract rightmost bit in the binary representation of endStep, this corresponds to size of maximally possible merge ending at endStep
span := spanStep * d.aggregationStep
if span > maxSpan {
span = maxSpan
}
start := item.endTxNum - span
if start < item.startTxNum {
if !minFound || start < startTxNum {
minFound = true
startTxNum = start
endTxNum = item.endTxNum
}
}
return true
})
return minFound, startTxNum, endTxNum
}
func (ii *InvertedIndex) findMergeRange(maxEndTxNum, maxSpan uint64) (bool, uint64, uint64) {
var minFound bool
var startTxNum, endTxNum uint64
ii.files.Ascend(func(i btree.Item) bool {
item := i.(*filesItem)
if item.endTxNum > maxEndTxNum {
return false
}
endStep := item.endTxNum / ii.aggregationStep
spanStep := endStep & -endStep // Extract rightmost bit in the binary representation of endStep, this corresponds to size of maximally possible merge ending at endStep
span := spanStep * ii.aggregationStep
if span > maxSpan {
span = maxSpan
}
start := item.endTxNum - span
if start < item.startTxNum {
if !minFound || start < startTxNum {
minFound = true
startTxNum = start
endTxNum = item.endTxNum
}
}
return true
})
return minFound, startTxNum, endTxNum
}
// staticFilesInRange returns list of static files with txNum in specified range [startTxNum; endTxNum)
// files are in the descending order of endTxNum
func (d *Domain) staticFilesInRange(startTxNum, endTxNum uint64) ([][NumberOfTypes]*filesItem, int) {
var files [][NumberOfTypes]*filesItem
var startJ int
for fType := FileType(0); fType < NumberOfTypes; fType++ {
startJ = 0
j := 0
d.files[fType].Ascend(func(i btree.Item) bool {
item := i.(*filesItem)
if item.startTxNum < startTxNum {
startJ++
return true
}
if item.endTxNum > endTxNum {
return false
}
for j >= len(files) {
files = append(files, [NumberOfTypes]*filesItem{})
}
files[j][fType] = item
j++
return true
})
}
return files, startJ
}
func (ii *InvertedIndex) staticFilesInRange(startTxNum, endTxNum uint64) ([]*filesItem, int) {
var files []*filesItem
var startJ int
j := 0
ii.files.Ascend(func(i btree.Item) bool {
item := i.(*filesItem)
if item.startTxNum < startTxNum {
startJ++
return true
}
if item.endTxNum > endTxNum {
return false
}
for j >= len(files) {
files = append(files, item)
}
j++
return true
})
return files, startJ
}
func mergeEfs(preval, val, buf []byte) ([]byte, error) {
preef, _ := eliasfano32.ReadEliasFano(preval)
ef, _ := eliasfano32.ReadEliasFano(val)
preIt := preef.Iterator()
efIt := ef.Iterator()
newEf := eliasfano32.NewEliasFano(preef.Count()+ef.Count(), ef.Max())
for preIt.HasNext() {
newEf.AddOffset(preIt.Next())
}
for efIt.HasNext() {
newEf.AddOffset(efIt.Next())
}
newEf.Build()
return newEf.AppendBytes(buf), nil
}
func (d *Domain) mergeFiles(files [][NumberOfTypes]*filesItem, startTxNum, endTxNum uint64, maxSpan uint64) ([NumberOfTypes]*filesItem, error) {
var outItems [NumberOfTypes]*filesItem
var comp *compress.Compressor
var decomp *compress.Decompressor
var err error
var closeItem bool = true
defer func() {
if closeItem {
if comp != nil {
comp.Close()
}
if decomp != nil {
decomp.Close()
}
for fType := FileType(0); fType < NumberOfTypes; fType++ {
outItem := outItems[fType]
if outItem != nil {
if outItem.decompressor != nil {
outItem.decompressor.Close()
}
if outItem.index != nil {
outItem.index.Close()
}
outItems[fType] = nil
}
}
}
}()
for fType := FileType(0); fType < NumberOfTypes; fType++ {
valCompressed := fType != EfHistory
removeVals := fType == History && (endTxNum-startTxNum) == maxSpan
tmpPath := filepath.Join(d.dir, fmt.Sprintf("%s-%s.%d-%d.tmp", d.filenameBase, fType.String(), startTxNum, endTxNum))
datPath := filepath.Join(d.dir, fmt.Sprintf("%s-%s.%d-%d.dat", d.filenameBase, fType.String(), startTxNum, endTxNum))
if removeVals {
if comp, err = compress.NewCompressor(context.Background(), "merge", tmpPath, d.dir, compress.MinPatternScore, 1, log.LvlDebug); err != nil {
return outItems, fmt.Errorf("merge %s history compressor: %w", d.filenameBase, err)
}
} else {
if comp, err = compress.NewCompressor(context.Background(), "merge", datPath, d.dir, compress.MinPatternScore, 1, log.LvlDebug); err != nil {
return outItems, fmt.Errorf("merge %s history compressor: %w", d.filenameBase, err)
}
}
var cp CursorHeap
heap.Init(&cp)
for _, filesByType := range files {
item := filesByType[fType]
g := item.decompressor.MakeGetter()
g.Reset(0)
if g.HasNext() {
key, _ := g.Next(nil)
val, _ := g.Next(nil)
heap.Push(&cp, &CursorItem{t: FILE_CURSOR, dg: g, key: key, val: val, endTxNum: item.endTxNum})
}
}
count := 0
// In the loop below, the pair `keyBuf=>valBuf` is always 1 item behind `lastKey=>lastVal`.
// `lastKey` and `lastVal` are taken from the top of the multi-way merge (assisted by the CursorHeap cp), but not processed right away
// instead, the pair from the previous iteration is processed first - `keyBuf=>valBuf`. After that, `keyBuf` and `valBuf` are assigned
// to `lastKey` and `lastVal` correspondingly, and the next step of multi-way merge happens. Therefore, after the multi-way merge loop
// (when CursorHeap cp is empty), there is a need to process the last pair `keyBuf=>valBuf`, because it was one step behind
var keyBuf, valBuf []byte
for cp.Len() > 0 {
lastKey := common.Copy(cp[0].key)
lastVal := common.Copy(cp[0].val)
var mergedOnce bool
// Advance all the items that have this key (including the top)
for cp.Len() > 0 && bytes.Equal(cp[0].key, lastKey) {
ci1 := cp[0]
if mergedOnce && fType == EfHistory {
if lastVal, err = mergeEfs(ci1.val, lastVal, nil); err != nil {
return outItems, fmt.Errorf("merge %s efhistory: %w", d.filenameBase, err)
}
} else {
mergedOnce = true
}
if ci1.dg.HasNext() {
ci1.key, _ = ci1.dg.Next(ci1.key[:0])
if valCompressed {
ci1.val, _ = ci1.dg.Next(ci1.val[:0])
} else {
ci1.val, _ = ci1.dg.NextUncompressed()
}
heap.Fix(&cp, 0)
} else {
heap.Pop(&cp)
}
}
var skip bool
if fType == Values {
if d.prefixLen > 0 {
skip = startTxNum == 0 && len(lastVal) == 0 && len(lastKey) != d.prefixLen
} else {
// For the rest of types, empty value means deletion
skip = startTxNum == 0 && len(lastVal) == 0
}
}
if !skip {
if keyBuf != nil && (d.prefixLen == 0 || len(keyBuf) != d.prefixLen || bytes.HasPrefix(lastKey, keyBuf)) {
if err = comp.AddWord(keyBuf); err != nil {
return outItems, err
}
count++ // Only counting keys, not values
if valCompressed {
if err = comp.AddWord(valBuf); err != nil {
return outItems, err
}
} else {
if err = comp.AddUncompressedWord(valBuf); err != nil {
return outItems, err
}
}
}
keyBuf = append(keyBuf[:0], lastKey...)
valBuf = append(valBuf[:0], lastVal...)
}
}
if keyBuf != nil {
if err = comp.AddWord(keyBuf); err != nil {
return outItems, err
}
count++ // Only counting keys, not values
if valCompressed {
if err = comp.AddWord(valBuf); err != nil {
return outItems, err
}
} else {
if err = comp.AddUncompressedWord(valBuf); err != nil {
return outItems, err
}
}
}
if err = comp.Compress(); err != nil {
return outItems, err
}
comp.Close()
comp = nil
idxPath := filepath.Join(d.dir, fmt.Sprintf("%s-%s.%d-%d.idx", d.filenameBase, fType.String(), startTxNum, endTxNum))
outItem := &filesItem{startTxNum: startTxNum, endTxNum: endTxNum}
outItems[fType] = outItem
if removeVals {
if comp, err = compress.NewCompressor(context.Background(), "merge", datPath, d.dir, compress.MinPatternScore, 1, log.LvlDebug); err != nil {
return outItems, fmt.Errorf("merge %s remove vals compressor: %w", d.filenameBase, err)
}
if decomp, err = compress.NewDecompressor(tmpPath); err != nil {
return outItems, fmt.Errorf("merge %s remove vals decompressor %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
g := decomp.MakeGetter()
var val []byte
var count int
g.Reset(0)
for g.HasNext() {
g.Skip() // Skip key on on the first pass
val, _ = g.Next(val[:0])
if err = comp.AddWord(val); err != nil {
return outItems, fmt.Errorf("merge %s remove vals add val %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
count++
}
if err = comp.Compress(); err != nil {
return outItems, err
}
comp = nil
if outItem.decompressor, err = compress.NewDecompressor(datPath); err != nil {
return outItems, fmt.Errorf("merge %s remove vals decompressor(no val) %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
var rs *recsplit.RecSplit
if rs, err = recsplit.NewRecSplit(recsplit.RecSplitArgs{
KeyCount: count,
Enums: false,
BucketSize: 2000,
LeafSize: 8,
TmpDir: d.dir,
StartSeed: []uint64{0x106393c187cae21a, 0x6453cec3f7376937, 0x643e521ddbd2be98, 0x3740c6412f6572cb, 0x717d47562f1ce470, 0x4cd6eb4c63befb7c, 0x9bfd8c5e18c8da73,
0x082f20e10092a9a3, 0x2ada2ce68d21defc, 0xe33cb4f3e7c6466b, 0x3980be458c509c59, 0xc466fd9584828e8c, 0x45f0aabe1a61ede6, 0xf6e7b8b33ad9b98d,
0x4ef95e25f4b4983d, 0x81175195173b92d3, 0x4e50927d8dd15978, 0x1ea2099d1fafae7f, 0x425c8a06fbaaa815, 0xcd4216006c74052a},
IndexFile: idxPath,
}); err != nil {
return outItems, fmt.Errorf("merge %s remove vals recsplit %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
g1 := outItem.decompressor.MakeGetter()
var key []byte
for {
g.Reset(0)
g1.Reset(0)
var lastOffset uint64
for g.HasNext() {
key, _ = g.Next(key[:0])
g.Skip() // Skip value
_, pos := g1.Next(nil)
if err = rs.AddKey(key, lastOffset); err != nil {
return outItems, fmt.Errorf("merge %s remove vals recsplit add key %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
lastOffset = pos
}
if err = rs.Build(); err != nil {
if rs.Collision() {
log.Info("Building reduceHistoryFiles. Collision happened. It's ok. Restarting...")
rs.ResetNextSalt()
} else {
return outItems, fmt.Errorf("merge %s remove vals recsplit build %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
} else {
break
}
}
decomp.Close()
decomp = nil
if outItem.index, err = recsplit.OpenIndex(idxPath); err != nil {
return outItems, fmt.Errorf("merge %s open index %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
} else {
if outItem.decompressor, err = compress.NewDecompressor(datPath); err != nil {
return outItems, fmt.Errorf("merge %s decompressor %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
if outItem.index, err = buildIndex(outItem.decompressor, idxPath, d.dir, count, fType == History /* values */); err != nil {
return outItems, fmt.Errorf("merge %s buildIndex %s [%d-%d]: %w", d.filenameBase, fType.String(), startTxNum, endTxNum, err)
}
}
outItem.getter = outItem.decompressor.MakeGetter()
outItem.getterMerge = outItem.decompressor.MakeGetter()
outItem.indexReader = recsplit.NewIndexReader(outItem.index)
outItem.readerMerge = recsplit.NewIndexReader(outItem.index)
}
closeItem = false
return outItems, nil
}
func (ii *InvertedIndex) mergeFiles(files []*filesItem, startTxNum, endTxNum uint64, maxSpan uint64) (*filesItem, error) {
var outItem *filesItem
var comp *compress.Compressor
var decomp *compress.Decompressor
var err error
var closeItem bool = true
defer func() {
if closeItem {
if comp != nil {
comp.Close()
}
if decomp != nil {
decomp.Close()
}
if outItem != nil {
if outItem.decompressor != nil {
outItem.decompressor.Close()
}
if outItem.index != nil {
outItem.index.Close()
}
outItem = nil
}
}
}()
datPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.dat", ii.filenameBase, startTxNum, endTxNum))
if comp, err = compress.NewCompressor(context.Background(), "merge", datPath, ii.dir, compress.MinPatternScore, 1, log.LvlDebug); err != nil {
return nil, fmt.Errorf("merge %s inverted index compressor: %w", ii.filenameBase, err)
}
var cp CursorHeap
heap.Init(&cp)
for _, item := range files {
g := item.decompressor.MakeGetter()
g.Reset(0)
if g.HasNext() {
key, _ := g.Next(nil)
val, _ := g.Next(nil)
heap.Push(&cp, &CursorItem{t: FILE_CURSOR, dg: g, key: key, val: val, endTxNum: item.endTxNum})
}
}
count := 0
// In the loop below, the pair `keyBuf=>valBuf` is always 1 item behind `lastKey=>lastVal`.
// `lastKey` and `lastVal` are taken from the top of the multi-way merge (assisted by the CursorHeap cp), but not processed right away
// instead, the pair from the previous iteration is processed first - `keyBuf=>valBuf`. After that, `keyBuf` and `valBuf` are assigned
// to `lastKey` and `lastVal` correspondingly, and the next step of multi-way merge happens. Therefore, after the multi-way merge loop
// (when CursorHeap cp is empty), there is a need to process the last pair `keyBuf=>valBuf`, because it was one step behind
var keyBuf, valBuf []byte
for cp.Len() > 0 {
lastKey := common.Copy(cp[0].key)
lastVal := common.Copy(cp[0].val)
var mergedOnce bool
// Advance all the items that have this key (including the top)
for cp.Len() > 0 && bytes.Equal(cp[0].key, lastKey) {
ci1 := cp[0]
if mergedOnce {
if lastVal, err = mergeEfs(ci1.val, lastVal, nil); err != nil {
return nil, fmt.Errorf("merge %s inverted index: %w", ii.filenameBase, err)
}
} else {
mergedOnce = true
}
if ci1.dg.HasNext() {
ci1.key, _ = ci1.dg.Next(ci1.key[:0])
ci1.val, _ = ci1.dg.NextUncompressed()
heap.Fix(&cp, 0)
} else {
heap.Pop(&cp)
}
}
if keyBuf != nil {
if err = comp.AddWord(keyBuf); err != nil {
return nil, err
}
count++ // Only counting keys, not values
if err = comp.AddUncompressedWord(valBuf); err != nil {
return nil, err
}
}
keyBuf = append(keyBuf[:0], lastKey...)
valBuf = append(valBuf[:0], lastVal...)
}
if keyBuf != nil {
if err = comp.AddWord(keyBuf); err != nil {
return nil, err
}
count++ // Only counting keys, not values
if err = comp.AddUncompressedWord(valBuf); err != nil {
return nil, err
}
}
if err = comp.Compress(); err != nil {
return nil, err
}
comp.Close()
comp = nil
idxPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.idx", ii.filenameBase, startTxNum, endTxNum))
outItem = &filesItem{startTxNum: startTxNum, endTxNum: endTxNum}
if outItem.decompressor, err = compress.NewDecompressor(datPath); err != nil {
return nil, fmt.Errorf("merge %s decompressor [%d-%d]: %w", ii.filenameBase, startTxNum, endTxNum, err)
}
if outItem.index, err = buildIndex(outItem.decompressor, idxPath, ii.dir, count, false /* values */); err != nil {
return nil, fmt.Errorf("merge %s buildIndex [%d-%d]: %w", ii.filenameBase, startTxNum, endTxNum, err)
}
outItem.getter = outItem.decompressor.MakeGetter()
outItem.getterMerge = outItem.decompressor.MakeGetter()
outItem.indexReader = recsplit.NewIndexReader(outItem.index)
outItem.readerMerge = recsplit.NewIndexReader(outItem.index)
closeItem = false
return outItem, nil
}
func (d *Domain) integrateMergedFiles(outs [][NumberOfTypes]*filesItem, in [NumberOfTypes]*filesItem) {
for fType := FileType(0); fType < NumberOfTypes; fType++ {
d.files[fType].ReplaceOrInsert(in[fType])
for _, out := range outs {
d.files[fType].Delete(out[fType])
out[fType].decompressor.Close()
out[fType].index.Close()
}
}
}
func (ii *InvertedIndex) integrateMergedFiles(outs []*filesItem, in *filesItem) {
ii.files.ReplaceOrInsert(in)
for _, out := range outs {
ii.files.Delete(out)
out.decompressor.Close()
out.index.Close()
}
}
func (d *Domain) deleteFiles(outs [][NumberOfTypes]*filesItem) error {
for fType := FileType(0); fType < NumberOfTypes; fType++ {
for _, out := range outs {
datPath := filepath.Join(d.dir, fmt.Sprintf("%s-%s.%d-%d.dat", d.filenameBase, fType.String(), out[fType].startTxNum, out[fType].endTxNum))
if err := os.Remove(datPath); err != nil {
return err
}
idxPath := filepath.Join(d.dir, fmt.Sprintf("%s-%s.%d-%d.idx", d.filenameBase, fType.String(), out[fType].startTxNum, out[fType].endTxNum))
if err := os.Remove(idxPath); err != nil {
return err
}
}
}
return nil
}
func (ii *InvertedIndex) deleteFiles(outs []*filesItem) error {
for _, out := range outs {
datPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.dat", ii.filenameBase, out.startTxNum, out.endTxNum))
if err := os.Remove(datPath); err != nil {
return err
}
idxPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.idx", ii.filenameBase, out.startTxNum, out.endTxNum))
if err := os.Remove(idxPath); err != nil {
return err
}
}
return nil
}