github.com/ledgerwatch/erigon-lib@v1.0.0/state/inverted_index.go (about)

     1  /*
     2     Copyright 2022 Erigon contributors
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package state
    18  
    19  import (
    20  	"bytes"
    21  	"container/heap"
    22  	"context"
    23  	"encoding/binary"
    24  	"fmt"
    25  	"math"
    26  	"os"
    27  	"path/filepath"
    28  	"regexp"
    29  	"strconv"
    30  	"sync/atomic"
    31  	"time"
    32  
    33  	"github.com/RoaringBitmap/roaring/roaring64"
    34  	"github.com/c2h5oh/datasize"
    35  	"github.com/ledgerwatch/erigon-lib/common/background"
    36  	"github.com/ledgerwatch/erigon-lib/common/cmp"
    37  	"github.com/ledgerwatch/erigon-lib/common/dir"
    38  	"github.com/ledgerwatch/erigon-lib/compress"
    39  	"github.com/ledgerwatch/erigon-lib/etl"
    40  	"github.com/ledgerwatch/erigon-lib/kv"
    41  	"github.com/ledgerwatch/erigon-lib/kv/bitmapdb"
    42  	"github.com/ledgerwatch/erigon-lib/kv/iter"
    43  	"github.com/ledgerwatch/erigon-lib/kv/order"
    44  	"github.com/ledgerwatch/erigon-lib/recsplit"
    45  	"github.com/ledgerwatch/erigon-lib/recsplit/eliasfano32"
    46  	"github.com/ledgerwatch/log/v3"
    47  	btree2 "github.com/tidwall/btree"
    48  	"golang.org/x/exp/slices"
    49  	"golang.org/x/sync/errgroup"
    50  )
    51  
    52  type InvertedIndex struct {
    53  	files *btree2.BTreeG[*filesItem] // thread-safe, but maybe need 1 RWLock for all trees in AggregatorV3
    54  
    55  	// roFiles derivative from field `file`, but without garbage (canDelete=true, overlaps, etc...)
    56  	// MakeContext() using this field in zero-copy way
    57  	roFiles atomic.Pointer[[]ctxItem]
    58  
    59  	indexKeysTable  string // txnNum_u64 -> key (k+auto_increment)
    60  	indexTable      string // k -> txnNum_u64 , Needs to be table with DupSort
    61  	dir, tmpdir     string // Directory where static files are created
    62  	filenameBase    string
    63  	aggregationStep uint64
    64  	compressWorkers int
    65  
    66  	integrityFileExtensions []string
    67  	withLocalityIndex       bool
    68  	localityIndex           *LocalityIndex
    69  	tx                      kv.RwTx
    70  
    71  	garbageFiles []*filesItem // files that exist on disk, but ignored on opening folder - because they are garbage
    72  
    73  	// fields for history write
    74  	txNum      uint64
    75  	txNumBytes [8]byte
    76  	wal        *invertedIndexWAL
    77  	logger     log.Logger
    78  
    79  	noFsync bool // fsync is enabled by default, but tests can manually disable
    80  }
    81  
    82  func NewInvertedIndex(
    83  	dir, tmpdir string,
    84  	aggregationStep uint64,
    85  	filenameBase string,
    86  	indexKeysTable string,
    87  	indexTable string,
    88  	withLocalityIndex bool,
    89  	integrityFileExtensions []string,
    90  	logger log.Logger,
    91  ) (*InvertedIndex, error) {
    92  	ii := InvertedIndex{
    93  		dir:                     dir,
    94  		tmpdir:                  tmpdir,
    95  		files:                   btree2.NewBTreeGOptions[*filesItem](filesItemLess, btree2.Options{Degree: 128, NoLocks: false}),
    96  		aggregationStep:         aggregationStep,
    97  		filenameBase:            filenameBase,
    98  		indexKeysTable:          indexKeysTable,
    99  		indexTable:              indexTable,
   100  		compressWorkers:         1,
   101  		integrityFileExtensions: integrityFileExtensions,
   102  		withLocalityIndex:       withLocalityIndex,
   103  		logger:                  logger,
   104  	}
   105  	ii.roFiles.Store(&[]ctxItem{})
   106  
   107  	if ii.withLocalityIndex {
   108  		var err error
   109  		ii.localityIndex, err = NewLocalityIndex(ii.dir, ii.tmpdir, ii.aggregationStep, ii.filenameBase, ii.logger)
   110  		if err != nil {
   111  			return nil, fmt.Errorf("NewHistory: %s, %w", ii.filenameBase, err)
   112  		}
   113  	}
   114  	return &ii, nil
   115  }
   116  
   117  func (ii *InvertedIndex) fileNamesOnDisk() ([]string, error) {
   118  	files, err := os.ReadDir(ii.dir)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	filteredFiles := make([]string, 0, len(files))
   123  	for _, f := range files {
   124  		if !f.Type().IsRegular() {
   125  			continue
   126  		}
   127  		filteredFiles = append(filteredFiles, f.Name())
   128  	}
   129  	return filteredFiles, nil
   130  }
   131  
   132  func (ii *InvertedIndex) OpenList(fNames []string) error {
   133  	if err := ii.localityIndex.OpenList(fNames); err != nil {
   134  		return err
   135  	}
   136  	ii.closeWhatNotInList(fNames)
   137  	ii.garbageFiles = ii.scanStateFiles(fNames)
   138  	if err := ii.openFiles(); err != nil {
   139  		return fmt.Errorf("NewHistory.openFiles: %s, %w", ii.filenameBase, err)
   140  	}
   141  	return nil
   142  }
   143  
   144  func (ii *InvertedIndex) OpenFolder() error {
   145  	files, err := ii.fileNamesOnDisk()
   146  	if err != nil {
   147  		return err
   148  	}
   149  	return ii.OpenList(files)
   150  }
   151  
   152  func (ii *InvertedIndex) scanStateFiles(fileNames []string) (garbageFiles []*filesItem) {
   153  	re := regexp.MustCompile("^" + ii.filenameBase + ".([0-9]+)-([0-9]+).ef$")
   154  	var err error
   155  Loop:
   156  	for _, name := range fileNames {
   157  		subs := re.FindStringSubmatch(name)
   158  		if len(subs) != 3 {
   159  			if len(subs) != 0 {
   160  				ii.logger.Warn("File ignored by inverted index scan, more than 3 submatches", "name", name, "submatches", len(subs))
   161  			}
   162  			continue
   163  		}
   164  		var startStep, endStep uint64
   165  		if startStep, err = strconv.ParseUint(subs[1], 10, 64); err != nil {
   166  			ii.logger.Warn("File ignored by inverted index scan, parsing startTxNum", "error", err, "name", name)
   167  			continue
   168  		}
   169  		if endStep, err = strconv.ParseUint(subs[2], 10, 64); err != nil {
   170  			ii.logger.Warn("File ignored by inverted index scan, parsing endTxNum", "error", err, "name", name)
   171  			continue
   172  		}
   173  		if startStep > endStep {
   174  			ii.logger.Warn("File ignored by inverted index scan, startTxNum > endTxNum", "name", name)
   175  			continue
   176  		}
   177  
   178  		startTxNum, endTxNum := startStep*ii.aggregationStep, endStep*ii.aggregationStep
   179  		var newFile = newFilesItem(startTxNum, endTxNum, ii.aggregationStep)
   180  
   181  		for _, ext := range ii.integrityFileExtensions {
   182  			requiredFile := fmt.Sprintf("%s.%d-%d.%s", ii.filenameBase, startStep, endStep, ext)
   183  			if !dir.FileExist(filepath.Join(ii.dir, requiredFile)) {
   184  				ii.logger.Debug(fmt.Sprintf("[snapshots] skip %s because %s doesn't exists", name, requiredFile))
   185  				garbageFiles = append(garbageFiles, newFile)
   186  				continue Loop
   187  			}
   188  		}
   189  
   190  		if _, has := ii.files.Get(newFile); has {
   191  			continue
   192  		}
   193  
   194  		addNewFile := true
   195  		var subSets []*filesItem
   196  		ii.files.Walk(func(items []*filesItem) bool {
   197  			for _, item := range items {
   198  				if item.isSubsetOf(newFile) {
   199  					subSets = append(subSets, item)
   200  					continue
   201  				}
   202  
   203  				if newFile.isSubsetOf(item) {
   204  					if item.frozen {
   205  						addNewFile = false
   206  						garbageFiles = append(garbageFiles, newFile)
   207  					}
   208  					continue
   209  				}
   210  			}
   211  			return true
   212  		})
   213  		//for _, subSet := range subSets {
   214  		//	ii.files.Delete(subSet)
   215  		//}
   216  		if addNewFile {
   217  			ii.files.Set(newFile)
   218  		}
   219  	}
   220  
   221  	return garbageFiles
   222  }
   223  
   224  func ctxFiles(files *btree2.BTreeG[*filesItem]) (roItems []ctxItem) {
   225  	roFiles := make([]ctxItem, 0, files.Len())
   226  	files.Walk(func(items []*filesItem) bool {
   227  		for _, item := range items {
   228  			if item.canDelete.Load() {
   229  				continue
   230  			}
   231  
   232  			// `kill -9` may leave small garbage files, but if big one already exists we assume it's good(fsynced) and no reason to merge again
   233  			// see super-set file, just drop sub-set files from list
   234  			for len(roFiles) > 0 && roFiles[len(roFiles)-1].src.isSubsetOf(item) {
   235  				roFiles[len(roFiles)-1].src = nil
   236  				roFiles = roFiles[:len(roFiles)-1]
   237  			}
   238  			roFiles = append(roFiles, ctxItem{
   239  				startTxNum: item.startTxNum,
   240  				endTxNum:   item.endTxNum,
   241  				i:          len(roFiles),
   242  				src:        item,
   243  			})
   244  		}
   245  		return true
   246  	})
   247  	if roFiles == nil {
   248  		roFiles = []ctxItem{}
   249  	}
   250  	return roFiles
   251  }
   252  
   253  func (ii *InvertedIndex) reCalcRoFiles() {
   254  	roFiles := ctxFiles(ii.files)
   255  	ii.roFiles.Store(&roFiles)
   256  }
   257  
   258  func (ii *InvertedIndex) missedIdxFiles() (l []*filesItem) {
   259  	ii.files.Walk(func(items []*filesItem) bool {
   260  		for _, item := range items {
   261  			fromStep, toStep := item.startTxNum/ii.aggregationStep, item.endTxNum/ii.aggregationStep
   262  			if !dir.FileExist(filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.efi", ii.filenameBase, fromStep, toStep))) {
   263  				l = append(l, item)
   264  			}
   265  		}
   266  		return true
   267  	})
   268  	return l
   269  }
   270  
   271  func (ii *InvertedIndex) buildEfi(ctx context.Context, item *filesItem, p *background.Progress) (err error) {
   272  	fromStep, toStep := item.startTxNum/ii.aggregationStep, item.endTxNum/ii.aggregationStep
   273  	fName := fmt.Sprintf("%s.%d-%d.efi", ii.filenameBase, fromStep, toStep)
   274  	idxPath := filepath.Join(ii.dir, fName)
   275  	p.Name.Store(&fName)
   276  	p.Total.Store(uint64(item.decompressor.Count()))
   277  	//ii.logger.Info("[snapshots] build idx", "file", fName)
   278  	return buildIndex(ctx, item.decompressor, idxPath, ii.tmpdir, item.decompressor.Count()/2, false, p, ii.logger, ii.noFsync)
   279  }
   280  
   281  // BuildMissedIndices - produce .efi/.vi/.kvi from .ef/.v/.kv
   282  func (ii *InvertedIndex) BuildMissedIndices(ctx context.Context, g *errgroup.Group, ps *background.ProgressSet) {
   283  	missedFiles := ii.missedIdxFiles()
   284  	for _, item := range missedFiles {
   285  		item := item
   286  		g.Go(func() error {
   287  			p := &background.Progress{}
   288  			ps.Add(p)
   289  			defer ps.Delete(p)
   290  			return ii.buildEfi(ctx, item, p)
   291  		})
   292  	}
   293  }
   294  
   295  func (ii *InvertedIndex) openFiles() error {
   296  	var err error
   297  	var totalKeys uint64
   298  	var invalidFileItems []*filesItem
   299  	ii.files.Walk(func(items []*filesItem) bool {
   300  		for _, item := range items {
   301  			if item.decompressor != nil {
   302  				continue
   303  			}
   304  			fromStep, toStep := item.startTxNum/ii.aggregationStep, item.endTxNum/ii.aggregationStep
   305  			datPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.ef", ii.filenameBase, fromStep, toStep))
   306  			if !dir.FileExist(datPath) {
   307  				invalidFileItems = append(invalidFileItems, item)
   308  				continue
   309  			}
   310  
   311  			if item.decompressor, err = compress.NewDecompressor(datPath); err != nil {
   312  				ii.logger.Debug("InvertedIndex.openFiles: %w, %s", err, datPath)
   313  				continue
   314  			}
   315  
   316  			if item.index != nil {
   317  				continue
   318  			}
   319  			idxPath := filepath.Join(ii.dir, fmt.Sprintf("%s.%d-%d.efi", ii.filenameBase, fromStep, toStep))
   320  			if dir.FileExist(idxPath) {
   321  				if item.index, err = recsplit.OpenIndex(idxPath); err != nil {
   322  					ii.logger.Debug("InvertedIndex.openFiles: %w, %s", err, idxPath)
   323  					return false
   324  				}
   325  				totalKeys += item.index.KeyCount()
   326  			}
   327  		}
   328  		return true
   329  	})
   330  	for _, item := range invalidFileItems {
   331  		ii.files.Delete(item)
   332  	}
   333  	if err != nil {
   334  		return err
   335  	}
   336  
   337  	ii.reCalcRoFiles()
   338  	return nil
   339  }
   340  
   341  func (ii *InvertedIndex) closeWhatNotInList(fNames []string) {
   342  	var toDelete []*filesItem
   343  	ii.files.Walk(func(items []*filesItem) bool {
   344  	Loop1:
   345  		for _, item := range items {
   346  			for _, protectName := range fNames {
   347  				if item.decompressor != nil && item.decompressor.FileName() == protectName {
   348  					continue Loop1
   349  				}
   350  			}
   351  			toDelete = append(toDelete, item)
   352  		}
   353  		return true
   354  	})
   355  	for _, item := range toDelete {
   356  		if item.decompressor != nil {
   357  			item.decompressor.Close()
   358  			item.decompressor = nil
   359  		}
   360  		if item.index != nil {
   361  			item.index.Close()
   362  			item.index = nil
   363  		}
   364  		ii.files.Delete(item)
   365  	}
   366  }
   367  
   368  func (ii *InvertedIndex) Close() {
   369  	ii.localityIndex.Close()
   370  	ii.closeWhatNotInList([]string{})
   371  	ii.reCalcRoFiles()
   372  }
   373  
   374  // DisableFsync - just for tests
   375  func (ii *InvertedIndex) DisableFsync() { ii.noFsync = true }
   376  
   377  func (ii *InvertedIndex) Files() (res []string) {
   378  	ii.files.Walk(func(items []*filesItem) bool {
   379  		for _, item := range items {
   380  			if item.decompressor != nil {
   381  				res = append(res, item.decompressor.FileName())
   382  			}
   383  		}
   384  		return true
   385  	})
   386  	return res
   387  }
   388  
   389  func (ii *InvertedIndex) SetTx(tx kv.RwTx) {
   390  	ii.tx = tx
   391  }
   392  
   393  func (ii *InvertedIndex) SetTxNum(txNum uint64) {
   394  	ii.txNum = txNum
   395  	binary.BigEndian.PutUint64(ii.txNumBytes[:], ii.txNum)
   396  }
   397  
   398  // Add - !NotThreadSafe. Must use WalRLock/BatchHistoryWriteEnd
   399  func (ii *InvertedIndex) Add(key []byte) error {
   400  	return ii.wal.add(key, key)
   401  }
   402  func (ii *InvertedIndex) add(key, indexKey []byte) error { //nolint
   403  	return ii.wal.add(key, indexKey)
   404  }
   405  
   406  func (ii *InvertedIndex) DiscardHistory(tmpdir string) {
   407  	ii.wal = ii.newWriter(tmpdir, false, true)
   408  }
   409  func (ii *InvertedIndex) StartWrites() {
   410  	ii.wal = ii.newWriter(ii.tmpdir, true, false)
   411  }
   412  func (ii *InvertedIndex) StartUnbufferedWrites() {
   413  	ii.wal = ii.newWriter(ii.tmpdir, false, false)
   414  }
   415  func (ii *InvertedIndex) FinishWrites() {
   416  	ii.wal.close()
   417  	ii.wal = nil
   418  }
   419  
   420  func (ii *InvertedIndex) Rotate() *invertedIndexWAL {
   421  	wal := ii.wal
   422  	if wal != nil {
   423  		ii.wal = ii.newWriter(ii.wal.tmpdir, ii.wal.buffered, ii.wal.discard)
   424  	}
   425  	return wal
   426  }
   427  
   428  type invertedIndexWAL struct {
   429  	ii        *InvertedIndex
   430  	index     *etl.Collector
   431  	indexKeys *etl.Collector
   432  	tmpdir    string
   433  	buffered  bool
   434  	discard   bool
   435  }
   436  
   437  // loadFunc - is analog of etl.Identity, but it signaling to etl - use .Put instead of .AppendDup - to allow duplicates
   438  // maybe in future we will improve etl, to sort dupSort values in the way that allow use .AppendDup
   439  func loadFunc(k, v []byte, table etl.CurrentTableReader, next etl.LoadNextFunc) error {
   440  	return next(k, k, v)
   441  }
   442  
   443  func (ii *invertedIndexWAL) Flush(ctx context.Context, tx kv.RwTx) error {
   444  	if ii.discard || !ii.buffered {
   445  		return nil
   446  	}
   447  	if err := ii.index.Load(tx, ii.ii.indexTable, loadFunc, etl.TransformArgs{Quit: ctx.Done()}); err != nil {
   448  		return err
   449  	}
   450  	if err := ii.indexKeys.Load(tx, ii.ii.indexKeysTable, loadFunc, etl.TransformArgs{Quit: ctx.Done()}); err != nil {
   451  		return err
   452  	}
   453  	ii.close()
   454  	return nil
   455  }
   456  
   457  func (ii *invertedIndexWAL) close() {
   458  	if ii == nil {
   459  		return
   460  	}
   461  	if ii.index != nil {
   462  		ii.index.Close()
   463  	}
   464  	if ii.indexKeys != nil {
   465  		ii.indexKeys.Close()
   466  	}
   467  }
   468  
   469  // 3 history + 4 indices = 10 etl collectors, 10*256Mb/8 = 512mb - for all indices buffers
   470  var WALCollectorRAM = 2 * (etl.BufferOptimalSize / 8)
   471  
   472  func init() {
   473  	v, _ := os.LookupEnv("ERIGON_WAL_COLLETOR_RAM")
   474  	if v != "" {
   475  		var err error
   476  		WALCollectorRAM, err = datasize.ParseString(v)
   477  		if err != nil {
   478  			panic(err)
   479  		}
   480  	}
   481  }
   482  
   483  func (ii *InvertedIndex) newWriter(tmpdir string, buffered, discard bool) *invertedIndexWAL {
   484  	w := &invertedIndexWAL{ii: ii,
   485  		buffered: buffered,
   486  		discard:  discard,
   487  		tmpdir:   tmpdir,
   488  	}
   489  	if buffered {
   490  		// etl collector doesn't fsync: means if have enough ram, all files produced by all collectors will be in ram
   491  		w.index = etl.NewCollector(ii.indexTable, tmpdir, etl.NewSortableBuffer(WALCollectorRAM), ii.logger)
   492  		w.indexKeys = etl.NewCollector(ii.indexKeysTable, tmpdir, etl.NewSortableBuffer(WALCollectorRAM), ii.logger)
   493  		w.index.LogLvl(log.LvlTrace)
   494  		w.indexKeys.LogLvl(log.LvlTrace)
   495  	}
   496  	return w
   497  }
   498  
   499  func (ii *invertedIndexWAL) add(key, indexKey []byte) error {
   500  	if ii.discard {
   501  		return nil
   502  	}
   503  
   504  	if ii.buffered {
   505  		if err := ii.indexKeys.Collect(ii.ii.txNumBytes[:], key); err != nil {
   506  			return err
   507  		}
   508  
   509  		if err := ii.index.Collect(indexKey, ii.ii.txNumBytes[:]); err != nil {
   510  			return err
   511  		}
   512  	} else {
   513  		if err := ii.ii.tx.Put(ii.ii.indexKeysTable, ii.ii.txNumBytes[:], key); err != nil {
   514  			return err
   515  		}
   516  		if err := ii.ii.tx.Put(ii.ii.indexTable, indexKey, ii.ii.txNumBytes[:]); err != nil {
   517  			return err
   518  		}
   519  	}
   520  	return nil
   521  }
   522  
   523  func (ii *InvertedIndex) MakeContext() *InvertedIndexContext {
   524  	var ic = InvertedIndexContext{
   525  		ii:    ii,
   526  		files: *ii.roFiles.Load(),
   527  		loc:   ii.localityIndex.MakeContext(),
   528  	}
   529  	for _, item := range ic.files {
   530  		if !item.src.frozen {
   531  			item.src.refcount.Add(1)
   532  		}
   533  	}
   534  	return &ic
   535  }
   536  func (ic *InvertedIndexContext) Close() {
   537  	for _, item := range ic.files {
   538  		if item.src.frozen {
   539  			continue
   540  		}
   541  		refCnt := item.src.refcount.Add(-1)
   542  		//GC: last reader responsible to remove useles files: close it and delete
   543  		if refCnt == 0 && item.src.canDelete.Load() {
   544  			item.src.closeFilesAndRemove()
   545  		}
   546  	}
   547  
   548  	for _, r := range ic.readers {
   549  		r.Close()
   550  	}
   551  
   552  	ic.loc.Close(ic.ii.logger)
   553  }
   554  
   555  type InvertedIndexContext struct {
   556  	ii      *InvertedIndex
   557  	files   []ctxItem // have no garbage (overlaps, etc...)
   558  	getters []*compress.Getter
   559  	readers []*recsplit.IndexReader
   560  	loc     *ctxLocalityIdx
   561  }
   562  
   563  func (ic *InvertedIndexContext) statelessGetter(i int) *compress.Getter {
   564  	if ic.getters == nil {
   565  		ic.getters = make([]*compress.Getter, len(ic.files))
   566  	}
   567  	r := ic.getters[i]
   568  	if r == nil {
   569  		r = ic.files[i].src.decompressor.MakeGetter()
   570  		ic.getters[i] = r
   571  	}
   572  	return r
   573  }
   574  func (ic *InvertedIndexContext) statelessIdxReader(i int) *recsplit.IndexReader {
   575  	if ic.readers == nil {
   576  		ic.readers = make([]*recsplit.IndexReader, len(ic.files))
   577  	}
   578  	r := ic.readers[i]
   579  	if r == nil {
   580  		r = ic.files[i].src.index.GetReaderFromPool()
   581  		ic.readers[i] = r
   582  	}
   583  	return r
   584  }
   585  
   586  func (ic *InvertedIndexContext) getFile(from, to uint64) (it ctxItem, ok bool) {
   587  	for _, item := range ic.files {
   588  		if item.startTxNum == from && item.endTxNum == to {
   589  			return item, true
   590  		}
   591  	}
   592  	return it, false
   593  }
   594  
   595  // IdxRange - return range of txNums for given `key`
   596  // is to be used in public API, therefore it relies on read-only transaction
   597  // so that iteration can be done even when the inverted index is being updated.
   598  // [startTxNum; endNumTx)
   599  func (ic *InvertedIndexContext) IdxRange(key []byte, startTxNum, endTxNum int, asc order.By, limit int, roTx kv.Tx) (iter.U64, error) {
   600  	frozenIt, err := ic.iterateRangeFrozen(key, startTxNum, endTxNum, asc, limit)
   601  	if err != nil {
   602  		return nil, err
   603  	}
   604  	recentIt, err := ic.recentIterateRange(key, startTxNum, endTxNum, asc, limit, roTx)
   605  	if err != nil {
   606  		return nil, err
   607  	}
   608  	return iter.Union[uint64](frozenIt, recentIt, asc, limit), nil
   609  }
   610  
   611  func (ic *InvertedIndexContext) recentIterateRange(key []byte, startTxNum, endTxNum int, asc order.By, limit int, roTx kv.Tx) (iter.U64, error) {
   612  	//optimization: return empty pre-allocated iterator if range is frozen
   613  	if asc {
   614  		isFrozenRange := len(ic.files) > 0 && endTxNum >= 0 && ic.files[len(ic.files)-1].endTxNum >= uint64(endTxNum)
   615  		if isFrozenRange {
   616  			return iter.EmptyU64, nil
   617  		}
   618  	} else {
   619  		isFrozenRange := len(ic.files) > 0 && startTxNum >= 0 && ic.files[len(ic.files)-1].endTxNum >= uint64(startTxNum)
   620  		if isFrozenRange {
   621  			return iter.EmptyU64, nil
   622  		}
   623  	}
   624  
   625  	var from []byte
   626  	if startTxNum >= 0 {
   627  		from = make([]byte, 8)
   628  		binary.BigEndian.PutUint64(from, uint64(startTxNum))
   629  	}
   630  
   631  	var to []byte
   632  	if endTxNum >= 0 {
   633  		to = make([]byte, 8)
   634  		binary.BigEndian.PutUint64(to, uint64(endTxNum))
   635  	}
   636  
   637  	it, err := roTx.RangeDupSort(ic.ii.indexTable, key, from, to, asc, limit)
   638  	if err != nil {
   639  		return nil, err
   640  	}
   641  	return iter.TransformKV2U64(it, func(_, v []byte) (uint64, error) {
   642  		return binary.BigEndian.Uint64(v), nil
   643  	}), nil
   644  }
   645  
   646  // IdxRange is to be used in public API, therefore it relies on read-only transaction
   647  // so that iteration can be done even when the inverted index is being updated.
   648  // [startTxNum; endNumTx)
   649  func (ic *InvertedIndexContext) iterateRangeFrozen(key []byte, startTxNum, endTxNum int, asc order.By, limit int) (*FrozenInvertedIdxIter, error) {
   650  	if asc && (startTxNum >= 0 && endTxNum >= 0) && startTxNum > endTxNum {
   651  		return nil, fmt.Errorf("startTxNum=%d epected to be lower than endTxNum=%d", startTxNum, endTxNum)
   652  	}
   653  	if !asc && (startTxNum >= 0 && endTxNum >= 0) && startTxNum < endTxNum {
   654  		return nil, fmt.Errorf("startTxNum=%d epected to be bigger than endTxNum=%d", startTxNum, endTxNum)
   655  	}
   656  
   657  	it := &FrozenInvertedIdxIter{
   658  		key:         key,
   659  		startTxNum:  startTxNum,
   660  		endTxNum:    endTxNum,
   661  		indexTable:  ic.ii.indexTable,
   662  		orderAscend: asc,
   663  		limit:       limit,
   664  		ef:          eliasfano32.NewEliasFano(1, 1),
   665  	}
   666  	if asc {
   667  		for i := len(ic.files) - 1; i >= 0; i-- {
   668  			// [from,to) && from < to
   669  			if endTxNum >= 0 && int(ic.files[i].startTxNum) >= endTxNum {
   670  				continue
   671  			}
   672  			if startTxNum >= 0 && ic.files[i].endTxNum <= uint64(startTxNum) {
   673  				break
   674  			}
   675  			it.stack = append(it.stack, ic.files[i])
   676  			it.stack[len(it.stack)-1].getter = it.stack[len(it.stack)-1].src.decompressor.MakeGetter()
   677  			it.stack[len(it.stack)-1].reader = it.stack[len(it.stack)-1].src.index.GetReaderFromPool()
   678  			it.hasNext = true
   679  		}
   680  	} else {
   681  		for i := 0; i < len(ic.files); i++ {
   682  			// [from,to) && from > to
   683  			if endTxNum >= 0 && int(ic.files[i].endTxNum) <= endTxNum {
   684  				continue
   685  			}
   686  			if startTxNum >= 0 && ic.files[i].startTxNum > uint64(startTxNum) {
   687  				break
   688  			}
   689  
   690  			it.stack = append(it.stack, ic.files[i])
   691  			it.stack[len(it.stack)-1].getter = it.stack[len(it.stack)-1].src.decompressor.MakeGetter()
   692  			it.stack[len(it.stack)-1].reader = it.stack[len(it.stack)-1].src.index.GetReaderFromPool()
   693  			it.hasNext = true
   694  		}
   695  	}
   696  	it.advance()
   697  	return it, nil
   698  }
   699  
   700  // FrozenInvertedIdxIter allows iteration over range of tx numbers
   701  // Iteration is not implmented via callback function, because there is often
   702  // a requirement for interators to be composable (for example, to implement AND and OR for indices)
   703  // FrozenInvertedIdxIter must be closed after use to prevent leaking of resources like cursor
   704  type FrozenInvertedIdxIter struct {
   705  	key                  []byte
   706  	startTxNum, endTxNum int
   707  	limit                int
   708  	orderAscend          order.By
   709  
   710  	efIt       iter.Unary[uint64]
   711  	indexTable string
   712  	stack      []ctxItem
   713  
   714  	nextN   uint64
   715  	hasNext bool
   716  	err     error
   717  
   718  	ef *eliasfano32.EliasFano
   719  }
   720  
   721  func (it *FrozenInvertedIdxIter) Close() {
   722  	for _, item := range it.stack {
   723  		item.reader.Close()
   724  	}
   725  }
   726  
   727  func (it *FrozenInvertedIdxIter) advance() {
   728  	if it.orderAscend {
   729  		if it.hasNext {
   730  			it.advanceInFiles()
   731  		}
   732  	} else {
   733  		if it.hasNext {
   734  			it.advanceInFiles()
   735  		}
   736  	}
   737  }
   738  
   739  func (it *FrozenInvertedIdxIter) HasNext() bool {
   740  	if it.err != nil { // always true, then .Next() call will return this error
   741  		return true
   742  	}
   743  	if it.limit == 0 { // limit reached
   744  		return false
   745  	}
   746  	return it.hasNext
   747  }
   748  
   749  func (it *FrozenInvertedIdxIter) Next() (uint64, error) { return it.next(), nil }
   750  
   751  func (it *FrozenInvertedIdxIter) next() uint64 {
   752  	it.limit--
   753  	n := it.nextN
   754  	it.advance()
   755  	return n
   756  }
   757  
   758  func (it *FrozenInvertedIdxIter) advanceInFiles() {
   759  	for {
   760  		for it.efIt == nil { //TODO: this loop may be optimized by LocalityIndex
   761  			if len(it.stack) == 0 {
   762  				it.hasNext = false
   763  				return
   764  			}
   765  			item := it.stack[len(it.stack)-1]
   766  			it.stack = it.stack[:len(it.stack)-1]
   767  			offset := item.reader.Lookup(it.key)
   768  			g := item.getter
   769  			g.Reset(offset)
   770  			k, _ := g.NextUncompressed()
   771  			if bytes.Equal(k, it.key) {
   772  				eliasVal, _ := g.NextUncompressed()
   773  				it.ef.Reset(eliasVal)
   774  				if it.orderAscend {
   775  					efiter := it.ef.Iterator()
   776  					if it.startTxNum > 0 {
   777  						efiter.Seek(uint64(it.startTxNum))
   778  					}
   779  					it.efIt = efiter
   780  				} else {
   781  					it.efIt = it.ef.ReverseIterator()
   782  				}
   783  			}
   784  		}
   785  
   786  		//TODO: add seek method
   787  		//Asc:  [from, to) AND from > to
   788  		//Desc: [from, to) AND from < to
   789  		if it.orderAscend {
   790  			for it.efIt.HasNext() {
   791  				n, _ := it.efIt.Next()
   792  				if it.endTxNum >= 0 && int(n) >= it.endTxNum {
   793  					it.hasNext = false
   794  					return
   795  				}
   796  				if int(n) >= it.startTxNum {
   797  					it.hasNext = true
   798  					it.nextN = n
   799  					return
   800  				}
   801  			}
   802  		} else {
   803  			for it.efIt.HasNext() {
   804  				n, _ := it.efIt.Next()
   805  				if int(n) <= it.endTxNum {
   806  					it.hasNext = false
   807  					return
   808  				}
   809  				if it.startTxNum >= 0 && int(n) <= it.startTxNum {
   810  					it.hasNext = true
   811  					it.nextN = n
   812  					return
   813  				}
   814  			}
   815  		}
   816  		it.efIt = nil // Exhausted this iterator
   817  	}
   818  }
   819  
   820  // RecentInvertedIdxIter allows iteration over range of tx numbers
   821  // Iteration is not implmented via callback function, because there is often
   822  // a requirement for interators to be composable (for example, to implement AND and OR for indices)
   823  type RecentInvertedIdxIter struct {
   824  	key                  []byte
   825  	startTxNum, endTxNum int
   826  	limit                int
   827  	orderAscend          order.By
   828  
   829  	roTx       kv.Tx
   830  	cursor     kv.CursorDupSort
   831  	indexTable string
   832  
   833  	nextN   uint64
   834  	hasNext bool
   835  	err     error
   836  
   837  	bm *roaring64.Bitmap
   838  }
   839  
   840  func (it *RecentInvertedIdxIter) Close() {
   841  	if it.cursor != nil {
   842  		it.cursor.Close()
   843  	}
   844  	bitmapdb.ReturnToPool64(it.bm)
   845  }
   846  
   847  func (it *RecentInvertedIdxIter) advanceInDB() {
   848  	var v []byte
   849  	var err error
   850  	if it.cursor == nil {
   851  		if it.cursor, err = it.roTx.CursorDupSort(it.indexTable); err != nil {
   852  			// TODO pass error properly around
   853  			panic(err)
   854  		}
   855  		var k []byte
   856  		if k, _, err = it.cursor.SeekExact(it.key); err != nil {
   857  			panic(err)
   858  		}
   859  		if k == nil {
   860  			it.hasNext = false
   861  			return
   862  		}
   863  		//Asc:  [from, to) AND from > to
   864  		//Desc: [from, to) AND from < to
   865  		var keyBytes [8]byte
   866  		if it.startTxNum > 0 {
   867  			binary.BigEndian.PutUint64(keyBytes[:], uint64(it.startTxNum))
   868  		}
   869  		if v, err = it.cursor.SeekBothRange(it.key, keyBytes[:]); err != nil {
   870  			panic(err)
   871  		}
   872  		if v == nil {
   873  			if !it.orderAscend {
   874  				_, v, _ = it.cursor.PrevDup()
   875  				if err != nil {
   876  					panic(err)
   877  				}
   878  			}
   879  			if v == nil {
   880  				it.hasNext = false
   881  				return
   882  			}
   883  		}
   884  	} else {
   885  		if it.orderAscend {
   886  			_, v, err = it.cursor.NextDup()
   887  			if err != nil {
   888  				// TODO pass error properly around
   889  				panic(err)
   890  			}
   891  		} else {
   892  			_, v, err = it.cursor.PrevDup()
   893  			if err != nil {
   894  				panic(err)
   895  			}
   896  		}
   897  	}
   898  
   899  	//Asc:  [from, to) AND from > to
   900  	//Desc: [from, to) AND from < to
   901  	if it.orderAscend {
   902  		for ; v != nil; _, v, err = it.cursor.NextDup() {
   903  			if err != nil {
   904  				// TODO pass error properly around
   905  				panic(err)
   906  			}
   907  			n := binary.BigEndian.Uint64(v)
   908  			if it.endTxNum >= 0 && int(n) >= it.endTxNum {
   909  				it.hasNext = false
   910  				return
   911  			}
   912  			if int(n) >= it.startTxNum {
   913  				it.hasNext = true
   914  				it.nextN = n
   915  				return
   916  			}
   917  		}
   918  	} else {
   919  		for ; v != nil; _, v, err = it.cursor.PrevDup() {
   920  			if err != nil {
   921  				// TODO pass error properly around
   922  				panic(err)
   923  			}
   924  			n := binary.BigEndian.Uint64(v)
   925  			if int(n) <= it.endTxNum {
   926  				it.hasNext = false
   927  				return
   928  			}
   929  			if it.startTxNum >= 0 && int(n) <= it.startTxNum {
   930  				it.hasNext = true
   931  				it.nextN = n
   932  				return
   933  			}
   934  		}
   935  	}
   936  
   937  	it.hasNext = false
   938  }
   939  
   940  func (it *RecentInvertedIdxIter) advance() {
   941  	if it.orderAscend {
   942  		if it.hasNext {
   943  			it.advanceInDB()
   944  		}
   945  	} else {
   946  		if it.hasNext {
   947  			it.advanceInDB()
   948  		}
   949  	}
   950  }
   951  
   952  func (it *RecentInvertedIdxIter) HasNext() bool {
   953  	if it.err != nil { // always true, then .Next() call will return this error
   954  		return true
   955  	}
   956  	if it.limit == 0 { // limit reached
   957  		return false
   958  	}
   959  	return it.hasNext
   960  }
   961  
   962  func (it *RecentInvertedIdxIter) Next() (uint64, error) {
   963  	if it.err != nil {
   964  		return 0, it.err
   965  	}
   966  	it.limit--
   967  	n := it.nextN
   968  	it.advance()
   969  	return n, nil
   970  }
   971  
   972  type InvertedIterator1 struct {
   973  	roTx           kv.Tx
   974  	cursor         kv.CursorDupSort
   975  	indexTable     string
   976  	key            []byte
   977  	h              ReconHeap
   978  	nextKey        []byte
   979  	nextFileKey    []byte
   980  	nextDbKey      []byte
   981  	endTxNum       uint64
   982  	startTxNum     uint64
   983  	startTxKey     [8]byte
   984  	hasNextInDb    bool
   985  	hasNextInFiles bool
   986  }
   987  
   988  func (it *InvertedIterator1) Close() {
   989  	if it.cursor != nil {
   990  		it.cursor.Close()
   991  	}
   992  }
   993  
   994  func (it *InvertedIterator1) advanceInFiles() {
   995  	for it.h.Len() > 0 {
   996  		top := heap.Pop(&it.h).(*ReconItem)
   997  		key := top.key
   998  		val, _ := top.g.NextUncompressed()
   999  		if top.g.HasNext() {
  1000  			top.key, _ = top.g.NextUncompressed()
  1001  			heap.Push(&it.h, top)
  1002  		}
  1003  		if !bytes.Equal(key, it.key) {
  1004  			ef, _ := eliasfano32.ReadEliasFano(val)
  1005  			min := ef.Get(0)
  1006  			max := ef.Max()
  1007  			if min < it.endTxNum && max >= it.startTxNum { // Intersection of [min; max) and [it.startTxNum; it.endTxNum)
  1008  				it.key = key
  1009  				it.nextFileKey = key
  1010  				return
  1011  			}
  1012  		}
  1013  	}
  1014  	it.hasNextInFiles = false
  1015  }
  1016  
  1017  func (it *InvertedIterator1) advanceInDb() {
  1018  	var k, v []byte
  1019  	var err error
  1020  	if it.cursor == nil {
  1021  		if it.cursor, err = it.roTx.CursorDupSort(it.indexTable); err != nil {
  1022  			// TODO pass error properly around
  1023  			panic(err)
  1024  		}
  1025  		if k, _, err = it.cursor.First(); err != nil {
  1026  			// TODO pass error properly around
  1027  			panic(err)
  1028  		}
  1029  	} else {
  1030  		if k, _, err = it.cursor.NextNoDup(); err != nil {
  1031  			panic(err)
  1032  		}
  1033  	}
  1034  	for k != nil {
  1035  		if v, err = it.cursor.SeekBothRange(k, it.startTxKey[:]); err != nil {
  1036  			panic(err)
  1037  		}
  1038  		if v != nil {
  1039  			txNum := binary.BigEndian.Uint64(v)
  1040  			if txNum < it.endTxNum {
  1041  				it.nextDbKey = append(it.nextDbKey[:0], k...)
  1042  				return
  1043  			}
  1044  		}
  1045  		if k, _, err = it.cursor.NextNoDup(); err != nil {
  1046  			panic(err)
  1047  		}
  1048  	}
  1049  	it.cursor.Close()
  1050  	it.cursor = nil
  1051  	it.hasNextInDb = false
  1052  }
  1053  
  1054  func (it *InvertedIterator1) advance() {
  1055  	if it.hasNextInFiles {
  1056  		if it.hasNextInDb {
  1057  			c := bytes.Compare(it.nextFileKey, it.nextDbKey)
  1058  			if c < 0 {
  1059  				it.nextKey = append(it.nextKey[:0], it.nextFileKey...)
  1060  				it.advanceInFiles()
  1061  			} else if c > 0 {
  1062  				it.nextKey = append(it.nextKey[:0], it.nextDbKey...)
  1063  				it.advanceInDb()
  1064  			} else {
  1065  				it.nextKey = append(it.nextKey[:0], it.nextFileKey...)
  1066  				it.advanceInDb()
  1067  				it.advanceInFiles()
  1068  			}
  1069  		} else {
  1070  			it.nextKey = append(it.nextKey[:0], it.nextFileKey...)
  1071  			it.advanceInFiles()
  1072  		}
  1073  	} else if it.hasNextInDb {
  1074  		it.nextKey = append(it.nextKey[:0], it.nextDbKey...)
  1075  		it.advanceInDb()
  1076  	} else {
  1077  		it.nextKey = nil
  1078  	}
  1079  }
  1080  
  1081  func (it *InvertedIterator1) HasNext() bool {
  1082  	return it.hasNextInFiles || it.hasNextInDb || it.nextKey != nil
  1083  }
  1084  
  1085  func (it *InvertedIterator1) Next(keyBuf []byte) []byte {
  1086  	result := append(keyBuf, it.nextKey...)
  1087  	it.advance()
  1088  	return result
  1089  }
  1090  
  1091  func (ic *InvertedIndexContext) IterateChangedKeys(startTxNum, endTxNum uint64, roTx kv.Tx) InvertedIterator1 {
  1092  	var ii1 InvertedIterator1
  1093  	ii1.hasNextInDb = true
  1094  	ii1.roTx = roTx
  1095  	ii1.indexTable = ic.ii.indexTable
  1096  	for _, item := range ic.files {
  1097  		if item.endTxNum <= startTxNum {
  1098  			continue
  1099  		}
  1100  		if item.startTxNum >= endTxNum {
  1101  			break
  1102  		}
  1103  		if item.endTxNum >= endTxNum {
  1104  			ii1.hasNextInDb = false
  1105  		}
  1106  		g := item.src.decompressor.MakeGetter()
  1107  		if g.HasNext() {
  1108  			key, _ := g.NextUncompressed()
  1109  			heap.Push(&ii1.h, &ReconItem{startTxNum: item.startTxNum, endTxNum: item.endTxNum, g: g, txNum: ^item.endTxNum, key: key})
  1110  			ii1.hasNextInFiles = true
  1111  		}
  1112  	}
  1113  	binary.BigEndian.PutUint64(ii1.startTxKey[:], startTxNum)
  1114  	ii1.startTxNum = startTxNum
  1115  	ii1.endTxNum = endTxNum
  1116  	ii1.advanceInDb()
  1117  	ii1.advanceInFiles()
  1118  	ii1.advance()
  1119  	return ii1
  1120  }
  1121  
  1122  func (ii *InvertedIndex) collate(ctx context.Context, txFrom, txTo uint64, roTx kv.Tx) (map[string]*roaring64.Bitmap, error) {
  1123  	keysCursor, err := roTx.CursorDupSort(ii.indexKeysTable)
  1124  	if err != nil {
  1125  		return nil, fmt.Errorf("create %s keys cursor: %w", ii.filenameBase, err)
  1126  	}
  1127  	defer keysCursor.Close()
  1128  	indexBitmaps := map[string]*roaring64.Bitmap{}
  1129  	var txKey [8]byte
  1130  	binary.BigEndian.PutUint64(txKey[:], txFrom)
  1131  	var k, v []byte
  1132  	for k, v, err = keysCursor.Seek(txKey[:]); err == nil && k != nil; k, v, err = keysCursor.Next() {
  1133  		txNum := binary.BigEndian.Uint64(k)
  1134  		if txNum >= txTo {
  1135  			break
  1136  		}
  1137  		var bitmap *roaring64.Bitmap
  1138  		var ok bool
  1139  		if bitmap, ok = indexBitmaps[string(v)]; !ok {
  1140  			bitmap = bitmapdb.NewBitmap64()
  1141  			indexBitmaps[string(v)] = bitmap
  1142  		}
  1143  		bitmap.Add(txNum)
  1144  
  1145  		select {
  1146  		case <-ctx.Done():
  1147  			return nil, ctx.Err()
  1148  		default:
  1149  		}
  1150  	}
  1151  	if err != nil {
  1152  		return nil, fmt.Errorf("iterate over %s keys cursor: %w", ii.filenameBase, err)
  1153  	}
  1154  	return indexBitmaps, nil
  1155  }
  1156  
  1157  type InvertedFiles struct {
  1158  	decomp *compress.Decompressor
  1159  	index  *recsplit.Index
  1160  }
  1161  
  1162  func (sf InvertedFiles) Close() {
  1163  	if sf.decomp != nil {
  1164  		sf.decomp.Close()
  1165  	}
  1166  	if sf.index != nil {
  1167  		sf.index.Close()
  1168  	}
  1169  }
  1170  
  1171  func (ii *InvertedIndex) buildFiles(ctx context.Context, step uint64, bitmaps map[string]*roaring64.Bitmap, ps *background.ProgressSet) (InvertedFiles, error) {
  1172  	var decomp *compress.Decompressor
  1173  	var index *recsplit.Index
  1174  	var comp *compress.Compressor
  1175  	var err error
  1176  	closeComp := true
  1177  	defer func() {
  1178  		if closeComp {
  1179  			if comp != nil {
  1180  				comp.Close()
  1181  			}
  1182  			if decomp != nil {
  1183  				decomp.Close()
  1184  			}
  1185  			if index != nil {
  1186  				index.Close()
  1187  			}
  1188  		}
  1189  	}()
  1190  	txNumFrom := step * ii.aggregationStep
  1191  	txNumTo := (step + 1) * ii.aggregationStep
  1192  	datFileName := fmt.Sprintf("%s.%d-%d.ef", ii.filenameBase, txNumFrom/ii.aggregationStep, txNumTo/ii.aggregationStep)
  1193  	datPath := filepath.Join(ii.dir, datFileName)
  1194  	keys := make([]string, 0, len(bitmaps))
  1195  	for key := range bitmaps {
  1196  		keys = append(keys, key)
  1197  	}
  1198  	slices.Sort(keys)
  1199  	{
  1200  		p := ps.AddNew(datFileName, 1)
  1201  		defer ps.Delete(p)
  1202  		comp, err = compress.NewCompressor(ctx, "ef", datPath, ii.tmpdir, compress.MinPatternScore, ii.compressWorkers, log.LvlTrace, ii.logger)
  1203  		if err != nil {
  1204  			return InvertedFiles{}, fmt.Errorf("create %s compressor: %w", ii.filenameBase, err)
  1205  		}
  1206  		var buf []byte
  1207  		for _, key := range keys {
  1208  			if err = comp.AddUncompressedWord([]byte(key)); err != nil {
  1209  				return InvertedFiles{}, fmt.Errorf("add %s key [%x]: %w", ii.filenameBase, key, err)
  1210  			}
  1211  			bitmap := bitmaps[key]
  1212  			ef := eliasfano32.NewEliasFano(bitmap.GetCardinality(), bitmap.Maximum())
  1213  			it := bitmap.Iterator()
  1214  			for it.HasNext() {
  1215  				ef.AddOffset(it.Next())
  1216  			}
  1217  			ef.Build()
  1218  			buf = ef.AppendBytes(buf[:0])
  1219  			if err = comp.AddUncompressedWord(buf); err != nil {
  1220  				return InvertedFiles{}, fmt.Errorf("add %s val: %w", ii.filenameBase, err)
  1221  			}
  1222  		}
  1223  		if err = comp.Compress(); err != nil {
  1224  			return InvertedFiles{}, fmt.Errorf("compress %s: %w", ii.filenameBase, err)
  1225  		}
  1226  		comp.Close()
  1227  		comp = nil
  1228  		ps.Delete(p)
  1229  	}
  1230  	if decomp, err = compress.NewDecompressor(datPath); err != nil {
  1231  		return InvertedFiles{}, fmt.Errorf("open %s decompressor: %w", ii.filenameBase, err)
  1232  	}
  1233  
  1234  	idxFileName := fmt.Sprintf("%s.%d-%d.efi", ii.filenameBase, txNumFrom/ii.aggregationStep, txNumTo/ii.aggregationStep)
  1235  	idxPath := filepath.Join(ii.dir, idxFileName)
  1236  	p := ps.AddNew(idxFileName, uint64(decomp.Count()*2))
  1237  	defer ps.Delete(p)
  1238  	if index, err = buildIndexThenOpen(ctx, decomp, idxPath, ii.tmpdir, len(keys), false /* values */, p, ii.logger, ii.noFsync); err != nil {
  1239  		return InvertedFiles{}, fmt.Errorf("build %s efi: %w", ii.filenameBase, err)
  1240  	}
  1241  	closeComp = false
  1242  	return InvertedFiles{decomp: decomp, index: index}, nil
  1243  }
  1244  
  1245  func (ii *InvertedIndex) integrateFiles(sf InvertedFiles, txNumFrom, txNumTo uint64) {
  1246  	fi := newFilesItem(txNumFrom, txNumTo, ii.aggregationStep)
  1247  	fi.decompressor = sf.decomp
  1248  	fi.index = sf.index
  1249  	ii.files.Set(fi)
  1250  
  1251  	ii.reCalcRoFiles()
  1252  }
  1253  
  1254  func (ii *InvertedIndex) warmup(ctx context.Context, txFrom, limit uint64, tx kv.Tx) error {
  1255  	keysCursor, err := tx.CursorDupSort(ii.indexKeysTable)
  1256  	if err != nil {
  1257  		return fmt.Errorf("create %s keys cursor: %w", ii.filenameBase, err)
  1258  	}
  1259  	defer keysCursor.Close()
  1260  	var txKey [8]byte
  1261  	binary.BigEndian.PutUint64(txKey[:], txFrom)
  1262  	var k, v []byte
  1263  	idxC, err := tx.CursorDupSort(ii.indexTable)
  1264  	if err != nil {
  1265  		return err
  1266  	}
  1267  	defer idxC.Close()
  1268  	k, v, err = keysCursor.Seek(txKey[:])
  1269  	if err != nil {
  1270  		return err
  1271  	}
  1272  	if k == nil {
  1273  		return nil
  1274  	}
  1275  	txFrom = binary.BigEndian.Uint64(k)
  1276  	txTo := txFrom + ii.aggregationStep
  1277  	if limit != math.MaxUint64 && limit != 0 {
  1278  		txTo = txFrom + limit
  1279  	}
  1280  	for ; k != nil; k, v, err = keysCursor.Next() {
  1281  		if err != nil {
  1282  			return fmt.Errorf("iterate over %s keys: %w", ii.filenameBase, err)
  1283  		}
  1284  		txNum := binary.BigEndian.Uint64(k)
  1285  		if txNum >= txTo {
  1286  			break
  1287  		}
  1288  		_, _ = idxC.SeekBothRange(v, k)
  1289  
  1290  		select {
  1291  		case <-ctx.Done():
  1292  			return ctx.Err()
  1293  		default:
  1294  		}
  1295  	}
  1296  	return nil
  1297  }
  1298  
  1299  // [txFrom; txTo)
  1300  func (ii *InvertedIndex) prune(ctx context.Context, txFrom, txTo, limit uint64, logEvery *time.Ticker) error {
  1301  	keysCursor, err := ii.tx.RwCursorDupSort(ii.indexKeysTable)
  1302  	if err != nil {
  1303  		return fmt.Errorf("create %s keys cursor: %w", ii.filenameBase, err)
  1304  	}
  1305  	defer keysCursor.Close()
  1306  	var txKey [8]byte
  1307  	binary.BigEndian.PutUint64(txKey[:], txFrom)
  1308  	k, v, err := keysCursor.Seek(txKey[:])
  1309  	if err != nil {
  1310  		return err
  1311  	}
  1312  	if k == nil {
  1313  		return nil
  1314  	}
  1315  	txFrom = binary.BigEndian.Uint64(k)
  1316  	if limit != math.MaxUint64 && limit != 0 {
  1317  		txTo = cmp.Min(txTo, txFrom+limit)
  1318  	}
  1319  	if txFrom >= txTo {
  1320  		return nil
  1321  	}
  1322  
  1323  	collector := etl.NewCollector("snapshots", ii.tmpdir, etl.NewOldestEntryBuffer(etl.BufferOptimalSize), ii.logger)
  1324  	defer collector.Close()
  1325  
  1326  	idxCForDeletes, err := ii.tx.RwCursorDupSort(ii.indexTable)
  1327  	if err != nil {
  1328  		return err
  1329  	}
  1330  	defer idxCForDeletes.Close()
  1331  	idxC, err := ii.tx.RwCursorDupSort(ii.indexTable)
  1332  	if err != nil {
  1333  		return err
  1334  	}
  1335  	defer idxC.Close()
  1336  
  1337  	// Invariant: if some `txNum=N` pruned - it's pruned Fully
  1338  	// Means: can use DeleteCurrentDuplicates all values of given `txNum`
  1339  	for ; k != nil; k, v, err = keysCursor.NextNoDup() {
  1340  		if err != nil {
  1341  			return err
  1342  		}
  1343  		txNum := binary.BigEndian.Uint64(k)
  1344  		if txNum >= txTo {
  1345  			break
  1346  		}
  1347  		for ; v != nil; _, v, err = keysCursor.NextDup() {
  1348  			if err != nil {
  1349  				return err
  1350  			}
  1351  			if err := collector.Collect(v, nil); err != nil {
  1352  				return err
  1353  			}
  1354  		}
  1355  
  1356  		// This DeleteCurrent needs to the last in the loop iteration, because it invalidates k and v
  1357  		if err = ii.tx.Delete(ii.indexKeysTable, k); err != nil {
  1358  			return err
  1359  		}
  1360  		select {
  1361  		case <-ctx.Done():
  1362  			return ctx.Err()
  1363  		default:
  1364  		}
  1365  	}
  1366  	if err != nil {
  1367  		return fmt.Errorf("iterate over %s keys: %w", ii.filenameBase, err)
  1368  	}
  1369  
  1370  	if err := collector.Load(ii.tx, "", func(key, _ []byte, table etl.CurrentTableReader, next etl.LoadNextFunc) error {
  1371  		for v, err := idxC.SeekBothRange(key, txKey[:]); v != nil; _, v, err = idxC.NextDup() {
  1372  			if err != nil {
  1373  				return err
  1374  			}
  1375  			txNum := binary.BigEndian.Uint64(v)
  1376  			if txNum >= txTo {
  1377  				break
  1378  			}
  1379  
  1380  			if _, _, err = idxCForDeletes.SeekBothExact(key, v); err != nil {
  1381  				return err
  1382  			}
  1383  			if err = idxCForDeletes.DeleteCurrent(); err != nil {
  1384  				return err
  1385  			}
  1386  
  1387  			select {
  1388  			case <-logEvery.C:
  1389  				ii.logger.Info("[snapshots] prune history", "name", ii.filenameBase, "to_step", fmt.Sprintf("%.2f", float64(txTo)/float64(ii.aggregationStep)), "prefix", fmt.Sprintf("%x", key[:8]))
  1390  			default:
  1391  			}
  1392  		}
  1393  		return nil
  1394  	}, etl.TransformArgs{}); err != nil {
  1395  		return err
  1396  	}
  1397  
  1398  	return nil
  1399  }
  1400  
  1401  func (ii *InvertedIndex) DisableReadAhead() {
  1402  	ii.files.Walk(func(items []*filesItem) bool {
  1403  		for _, item := range items {
  1404  			item.decompressor.DisableReadAhead()
  1405  			if item.index != nil {
  1406  				item.index.DisableReadAhead()
  1407  			}
  1408  		}
  1409  		return true
  1410  	})
  1411  }
  1412  
  1413  func (ii *InvertedIndex) EnableReadAhead() *InvertedIndex {
  1414  	ii.files.Walk(func(items []*filesItem) bool {
  1415  		for _, item := range items {
  1416  			item.decompressor.EnableReadAhead()
  1417  			if item.index != nil {
  1418  				item.index.EnableReadAhead()
  1419  			}
  1420  		}
  1421  		return true
  1422  	})
  1423  	return ii
  1424  }
  1425  func (ii *InvertedIndex) EnableMadvWillNeed() *InvertedIndex {
  1426  	ii.files.Walk(func(items []*filesItem) bool {
  1427  		for _, item := range items {
  1428  			item.decompressor.EnableWillNeed()
  1429  			if item.index != nil {
  1430  				item.index.EnableWillNeed()
  1431  			}
  1432  		}
  1433  		return true
  1434  	})
  1435  	return ii
  1436  }
  1437  func (ii *InvertedIndex) EnableMadvNormalReadAhead() *InvertedIndex {
  1438  	ii.files.Walk(func(items []*filesItem) bool {
  1439  		for _, item := range items {
  1440  			item.decompressor.EnableMadvNormal()
  1441  			if item.index != nil {
  1442  				item.index.EnableMadvNormal()
  1443  			}
  1444  		}
  1445  		return true
  1446  	})
  1447  	return ii
  1448  }
  1449  
  1450  func (ii *InvertedIndex) collectFilesStat() (filesCount, filesSize, idxSize uint64) {
  1451  	if ii.files == nil {
  1452  		return 0, 0, 0
  1453  	}
  1454  	ii.files.Walk(func(items []*filesItem) bool {
  1455  		for _, item := range items {
  1456  			if item.index == nil {
  1457  				return false
  1458  			}
  1459  			filesSize += uint64(item.decompressor.Size())
  1460  			idxSize += uint64(item.index.Size())
  1461  			filesCount += 2
  1462  		}
  1463  		return true
  1464  	})
  1465  	return filesCount, filesSize, idxSize
  1466  }
  1467  
  1468  func (ii *InvertedIndex) stepsRangeInDBAsStr(tx kv.Tx) string {
  1469  	a1, a2 := ii.stepsRangeInDB(tx)
  1470  	return fmt.Sprintf("%s: %.1f-%.1f", ii.filenameBase, a1, a2)
  1471  }
  1472  func (ii *InvertedIndex) stepsRangeInDB(tx kv.Tx) (from, to float64) {
  1473  	fst, _ := kv.FirstKey(tx, ii.indexKeysTable)
  1474  	if len(fst) > 0 {
  1475  		from = float64(binary.BigEndian.Uint64(fst)) / float64(ii.aggregationStep)
  1476  	}
  1477  	lst, _ := kv.LastKey(tx, ii.indexKeysTable)
  1478  	if len(lst) > 0 {
  1479  		to = float64(binary.BigEndian.Uint64(lst)) / float64(ii.aggregationStep)
  1480  	}
  1481  	return from, to
  1482  }