/* 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" "encoding/binary" "fmt" "os" "path/filepath" "github.com/ledgerwatch/log/v3" "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" ) func (d *Domain) endTxNumMinimax() uint64 { minimax := d.History.endTxNumMinimax() if max, ok := d.files.Max(); ok { endTxNum := max.endTxNum if minimax == 0 || endTxNum < minimax { minimax = endTxNum } } return minimax } func (ii *InvertedIndex) endTxNumMinimax() uint64 { var minimax uint64 if max, ok := ii.files.Max(); ok { endTxNum := max.endTxNum if minimax == 0 || endTxNum < minimax { minimax = endTxNum } } return minimax } func (h *History) endTxNumMinimax() uint64 { minimax := h.InvertedIndex.endTxNumMinimax() if max, ok := h.files.Max(); ok { endTxNum := max.endTxNum if minimax == 0 || endTxNum < minimax { minimax = endTxNum } } return minimax } type DomainRanges struct { valuesStartTxNum uint64 valuesEndTxNum uint64 values bool historyStartTxNum uint64 historyEndTxNum uint64 history bool indexStartTxNum uint64 indexEndTxNum uint64 index bool } func (r DomainRanges) any() bool { return r.values || r.history || r.index } // 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) DomainRanges { hr := d.History.findMergeRange(maxEndTxNum, maxSpan) r := DomainRanges{ historyStartTxNum: hr.historyStartTxNum, historyEndTxNum: hr.historyEndTxNum, history: hr.history, indexStartTxNum: hr.indexStartTxNum, indexEndTxNum: hr.indexEndTxNum, index: hr.index, } d.files.Ascend(func(item *filesItem) bool { 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 start := item.endTxNum - span if start < item.startTxNum { if !r.values || start < r.valuesStartTxNum { r.values = true r.valuesStartTxNum = start r.valuesEndTxNum = item.endTxNum } } return true }) return r } func (ii *InvertedIndex) findMergeRange(maxEndTxNum, maxSpan uint64) (bool, uint64, uint64) { var minFound bool var startTxNum, endTxNum uint64 ii.files.Ascend(func(item *filesItem) bool { 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 } type HistoryRanges struct { historyStartTxNum uint64 historyEndTxNum uint64 history bool indexStartTxNum uint64 indexEndTxNum uint64 index bool } func (r HistoryRanges) any() bool { return r.history || r.index } func (h *History) findMergeRange(maxEndTxNum, maxSpan uint64) HistoryRanges { var r HistoryRanges r.index, r.indexStartTxNum, r.indexEndTxNum = h.InvertedIndex.findMergeRange(maxEndTxNum, maxSpan) h.files.Ascend(func(item *filesItem) bool { if item.endTxNum > maxEndTxNum { return false } endStep := item.endTxNum / h.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 * h.aggregationStep if span > maxSpan { span = maxSpan } start := item.endTxNum - span if start < item.startTxNum { if !r.history || start < r.historyStartTxNum { r.history = true r.historyStartTxNum = start r.historyEndTxNum = item.endTxNum } } return true }) return r } // 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(r DomainRanges) (valuesFiles, indexFiles, historyFiles []*filesItem, startJ int) { if r.index || r.history { indexFiles, historyFiles, startJ = d.History.staticFilesInRange(HistoryRanges{ historyStartTxNum: r.historyStartTxNum, historyEndTxNum: r.historyEndTxNum, history: r.history, indexStartTxNum: r.indexStartTxNum, indexEndTxNum: r.indexEndTxNum, index: r.index, }) } if r.values { d.files.Ascend(func(item *filesItem) bool { if item.startTxNum < r.valuesStartTxNum { startJ++ return true } if item.endTxNum > r.valuesEndTxNum { return false } valuesFiles = append(valuesFiles, item) return true }) } return } func (ii *InvertedIndex) staticFilesInRange(startTxNum, endTxNum uint64) ([]*filesItem, int) { var files []*filesItem var startJ int ii.files.Ascend(func(item *filesItem) bool { if item.startTxNum < startTxNum { startJ++ return true } if item.endTxNum > endTxNum { return false } files = append(files, item) return true }) return files, startJ } func (h *History) staticFilesInRange(r HistoryRanges) (indexFiles, historyFiles []*filesItem, startJ int) { if r.index { indexFiles, startJ = h.InvertedIndex.staticFilesInRange(r.indexStartTxNum, r.indexEndTxNum) } if r.history { startJ = 0 h.files.Ascend(func(item *filesItem) bool { if item.startTxNum < r.historyStartTxNum { startJ++ return true } if item.endTxNum > r.historyEndTxNum { return false } historyFiles = append(historyFiles, item) return true }) } return } 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(valuesFiles, indexFiles, historyFiles []*filesItem, r DomainRanges, maxSpan uint64) (valuesIn, indexIn, historyIn *filesItem, err error) { if !r.any() { return } var comp *compress.Compressor var decomp *compress.Decompressor var closeItem bool = true defer func() { if closeItem { if comp != nil { comp.Close() } if decomp != nil { decomp.Close() } if indexIn != nil { if indexIn.decompressor != nil { indexIn.decompressor.Close() } if indexIn.index != nil { indexIn.index.Close() } } if historyIn != nil { if historyIn.decompressor != nil { historyIn.decompressor.Close() } if historyIn.index != nil { historyIn.index.Close() } } if valuesIn != nil { if valuesIn.decompressor != nil { valuesIn.decompressor.Close() } if valuesIn.index != nil { valuesIn.index.Close() } } } }() if indexIn, historyIn, err = d.History.mergeFiles(indexFiles, historyFiles, HistoryRanges{ historyStartTxNum: r.historyStartTxNum, historyEndTxNum: r.historyEndTxNum, history: r.history, indexStartTxNum: r.indexStartTxNum, indexEndTxNum: r.indexEndTxNum, index: r.index}, maxSpan); err != nil { return nil, nil, nil, err } if r.values { datPath := filepath.Join(d.dir, fmt.Sprintf("%s.%d-%d.kv", d.filenameBase, r.valuesStartTxNum/d.aggregationStep, r.valuesEndTxNum/d.aggregationStep)) if comp, err = compress.NewCompressor(context.Background(), "merge", datPath, d.dir, compress.MinPatternScore, d.workers, log.LvlDebug); err != nil { return nil, nil, nil, fmt.Errorf("merge %s history compressor: %w", d.filenameBase, err) } var cp CursorHeap heap.Init(&cp) for _, item := range valuesFiles { g := item.decompressor.MakeGetter() g.Reset(0) if g.HasNext() { key, _ := g.NextUncompressed() var val []byte if d.compressVals { val, _ = g.Next(nil) } else { val, _ = g.NextUncompressed() } heap.Push(&cp, &CursorItem{ t: FILE_CURSOR, dg: g, key: key, val: val, endTxNum: item.endTxNum, reverse: true, }) } } 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) // 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 ci1.dg.HasNext() { ci1.key, _ = ci1.dg.NextUncompressed() if d.compressVals { 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 d.prefixLen > 0 { skip = r.valuesStartTxNum == 0 && len(lastVal) == 0 && len(lastKey) != d.prefixLen } else { // For the rest of types, empty value means deletion skip = r.valuesStartTxNum == 0 && len(lastVal) == 0 } if !skip { if keyBuf != nil && (d.prefixLen == 0 || len(keyBuf) != d.prefixLen || bytes.HasPrefix(lastKey, keyBuf)) { if err = comp.AddUncompressedWord(keyBuf); err != nil { return nil, nil, nil, err } count++ // Only counting keys, not values //if d.valueMergeFn != nil { // valBuf, err = d.valueMergeFn(valBuf, nil) // if err != nil { // return nil, nil, nil, err // } //} if d.compressVals { if err = comp.AddWord(valBuf); err != nil { return nil, nil, nil, err } } else { if err = comp.AddUncompressedWord(valBuf); err != nil { return nil, nil, nil, err } } } keyBuf = append(keyBuf[:0], lastKey...) valBuf = append(valBuf[:0], lastVal...) } } if keyBuf != nil { if err = comp.AddUncompressedWord(keyBuf); err != nil { return nil, nil, nil, err } count++ // Only counting keys, not values if d.compressVals { if err = comp.AddWord(valBuf); err != nil { return nil, nil, nil, err } } else { if err = comp.AddUncompressedWord(valBuf); err != nil { return nil, nil, nil, err } } } if err = comp.Compress(); err != nil { return nil, nil, nil, err } comp.Close() comp = nil idxPath := filepath.Join(d.dir, fmt.Sprintf("%s.%d-%d.kvi", d.filenameBase, r.valuesStartTxNum/d.aggregationStep, r.valuesEndTxNum/d.aggregationStep)) valuesIn = &filesItem{startTxNum: r.valuesStartTxNum, endTxNum: r.valuesEndTxNum} if valuesIn.decompressor, err = compress.NewDecompressor(datPath); err != nil { return nil, nil, nil, fmt.Errorf("merge %s decompressor [%d-%d]: %w", d.filenameBase, r.valuesStartTxNum, r.valuesEndTxNum, err) } if valuesIn.index, err = buildIndex(valuesIn.decompressor, idxPath, d.dir, count, false /* values */); err != nil { return nil, nil, nil, fmt.Errorf("merge %s buildIndex [%d-%d]: %w", d.filenameBase, r.valuesStartTxNum, r.valuesEndTxNum, err) } } closeItem = false d.stats.MergesCount++ d.mergesCount++ return } //func (d *Domain) SetValueMergeStrategy(merge func([]byte, []byte) ([]byte, error)) { // d.valueMergeFn = merge //} 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.ef", ii.filenameBase, startTxNum/ii.aggregationStep, endTxNum/ii.aggregationStep)) if comp, err = compress.NewCompressor(context.Background(), "Snapshots merge", datPath, ii.dir, compress.MinPatternScore, ii.workers, 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, reverse: true, }) } } 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.NextUncompressed() ci1.val, _ = ci1.dg.NextUncompressed() heap.Fix(&cp, 0) } else { heap.Pop(&cp) } } if keyBuf != nil { if err = comp.AddUncompressedWord(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.AddUncompressedWord(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.efi", ii.filenameBase, startTxNum/ii.aggregationStep, endTxNum/ii.aggregationStep)) 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) } closeItem = false return outItem, nil } func (h *History) mergeFiles(indexFiles, historyFiles []*filesItem, r HistoryRanges, maxSpan uint64) (indexIn, historyIn *filesItem, err error) { if !r.any() { return nil, nil, nil } var closeIndex = true defer func() { if closeIndex { if indexIn != nil { indexIn.decompressor.Close() indexIn.index.Close() } } }() if indexIn, err = h.InvertedIndex.mergeFiles(indexFiles, r.indexStartTxNum, r.indexEndTxNum, maxSpan); err != nil { return nil, nil, err } if r.history { var comp *compress.Compressor var decomp *compress.Decompressor var rs *recsplit.RecSplit var index *recsplit.Index var closeItem bool = true defer func() { if closeItem { if comp != nil { comp.Close() } if decomp != nil { decomp.Close() } if rs != nil { rs.Close() } if index != nil { index.Close() } if historyIn != nil { if historyIn.decompressor != nil { historyIn.decompressor.Close() } if historyIn.index != nil { historyIn.index.Close() } } } }() datPath := filepath.Join(h.dir, fmt.Sprintf("%s.%d-%d.v", h.filenameBase, r.historyStartTxNum/h.aggregationStep, r.historyEndTxNum/h.aggregationStep)) idxPath := filepath.Join(h.dir, fmt.Sprintf("%s.%d-%d.vi", h.filenameBase, r.historyStartTxNum/h.aggregationStep, r.historyEndTxNum/h.aggregationStep)) if comp, err = compress.NewCompressor(context.Background(), "merge", datPath, h.dir, compress.MinPatternScore, h.workers, log.LvlDebug); err != nil { return nil, nil, fmt.Errorf("merge %s history compressor: %w", h.filenameBase, err) } var cp CursorHeap heap.Init(&cp) for i, item := range indexFiles { g := item.decompressor.MakeGetter() g.Reset(0) if g.HasNext() { g2 := historyFiles[i].decompressor.MakeGetter() key, _ := g.NextUncompressed() val, _ := g.NextUncompressed() heap.Push(&cp, &CursorItem{ t: FILE_CURSOR, dg: g, dg2: g2, key: key, val: val, endTxNum: item.endTxNum, reverse: false, }) } } 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 valBuf []byte for cp.Len() > 0 { lastKey := common.Copy(cp[0].key) // Advance all the items that have this key (including the top) //var mergeOnce bool for cp.Len() > 0 && bytes.Equal(cp[0].key, lastKey) { ci1 := cp[0] //if h.valueMergeFn != nil && mergeOnce { // valBuf, err = h.valueMergeFn(ci1.val, valBuf) // if err != nil { // return nil, nil, err // } // ci1.val = valBuf //} //if !mergeOnce { // mergeOnce = true //} ef, _ := eliasfano32.ReadEliasFano(ci1.val) for i := uint64(0); i < ef.Count(); i++ { if h.compressVals { valBuf, _ = ci1.dg2.Next(valBuf[:0]) if err = comp.AddWord(valBuf); err != nil { return nil, nil, err } } else { valBuf, _ = ci1.dg2.NextUncompressed() if err = comp.AddUncompressedWord(valBuf); err != nil { return nil, nil, err } } } count += int(ef.Count()) if ci1.dg.HasNext() { ci1.key, _ = ci1.dg.NextUncompressed() ci1.val, _ = ci1.dg.NextUncompressed() heap.Fix(&cp, 0) } else { heap.Remove(&cp, 0) } } } if err = comp.Compress(); err != nil { return nil, nil, err } comp.Close() comp = nil if decomp, err = compress.NewDecompressor(datPath); err != nil { return nil, nil, err } if rs, err = recsplit.NewRecSplit(recsplit.RecSplitArgs{ KeyCount: count, Enums: false, BucketSize: 2000, LeafSize: 8, TmpDir: h.dir, IndexFile: idxPath, }); err != nil { return nil, nil, fmt.Errorf("create recsplit: %w", err) } var historyKey []byte var txKey [8]byte var valOffset uint64 g := indexIn.decompressor.MakeGetter() g2 := decomp.MakeGetter() var keyBuf []byte for { g.Reset(0) g2.Reset(0) valOffset = 0 for g.HasNext() { keyBuf, _ = g.NextUncompressed() valBuf, _ = g.NextUncompressed() ef, _ := eliasfano32.ReadEliasFano(valBuf) efIt := ef.Iterator() for efIt.HasNext() { txNum := efIt.Next() binary.BigEndian.PutUint64(txKey[:], txNum) historyKey = append(append(historyKey[:0], txKey[:]...), keyBuf...) if err = rs.AddKey(historyKey, valOffset); err != nil { return nil, nil, err } if h.compressVals { valOffset = g2.Skip() } else { valOffset = g2.SkipUncompressed() } } } if err = rs.Build(); err != nil { if rs.Collision() { log.Info("Building recsplit. Collision happened. It's ok. Restarting...") rs.ResetNextSalt() } else { return nil, nil, fmt.Errorf("build %s idx: %w", h.filenameBase, err) } } else { break } } rs.Close() rs = nil if index, err = recsplit.OpenIndex(idxPath); err != nil { return nil, nil, fmt.Errorf("open %s idx: %w", h.filenameBase, err) } historyIn = &filesItem{startTxNum: r.historyStartTxNum, endTxNum: r.historyEndTxNum, decompressor: decomp, index: index} closeItem = false } closeIndex = false return } func (d *Domain) integrateMergedFiles(valuesOuts, indexOuts, historyOuts []*filesItem, valuesIn, indexIn, historyIn *filesItem) { d.History.integrateMergedFiles(indexOuts, historyOuts, indexIn, historyIn) d.files.ReplaceOrInsert(valuesIn) for _, out := range valuesOuts { d.files.Delete(out) out.decompressor.Close() out.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 (h *History) integrateMergedFiles(indexOuts, historyOuts []*filesItem, indexIn, historyIn *filesItem) { h.InvertedIndex.integrateMergedFiles(indexOuts, indexIn) h.files.ReplaceOrInsert(historyIn) for _, out := range historyOuts { h.files.Delete(out) out.decompressor.Close() out.index.Close() } } func (d *Domain) deleteFiles(valuesOuts, indexOuts, historyOuts []*filesItem) error { if err := d.History.deleteFiles(indexOuts, historyOuts); err != nil { return err } for _, out := range valuesOuts { datPath := filepath.Join(d.dir, fmt.Sprintf("%s.%d-%d.kv", d.filenameBase, out.startTxNum/d.aggregationStep, out.endTxNum/d.aggregationStep)) if err := os.Remove(datPath); err != nil { return err } idxPath := filepath.Join(d.dir, fmt.Sprintf("%s.%d-%d.kvi", d.filenameBase, out.startTxNum/d.aggregationStep, out.endTxNum/d.aggregationStep)) 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.ef", ii.filenameBase, out.startTxNum/ii.aggregationStep, out.endTxNum/ii.aggregationStep)) if err := os.Remove(datPath); err != nil { return err } idxPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.efi", ii.filenameBase, out.startTxNum/ii.aggregationStep, out.endTxNum/ii.aggregationStep)) if err := os.Remove(idxPath); err != nil { return err } } return nil } func (h *History) deleteFiles(indexOuts, historyOuts []*filesItem) error { if err := h.InvertedIndex.deleteFiles(indexOuts); err != nil { return err } for _, out := range historyOuts { datPath := filepath.Join(h.dir, fmt.Sprintf("%s.%d-%d.v", h.filenameBase, out.startTxNum/h.aggregationStep, out.endTxNum/h.aggregationStep)) if err := os.Remove(datPath); err != nil { return err } idxPath := filepath.Join(h.dir, fmt.Sprintf("%s.%d-%d.vi", h.filenameBase, out.startTxNum/h.aggregationStep, out.endTxNum/h.aggregationStep)) if err := os.Remove(idxPath); err != nil { return err } } return nil }