mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-25 06:06:44 +00:00
core/rawdb: move tx indexing stuff to txindexer
This commit is contained in:
parent
83366fdf5c
commit
12d163c5bd
3 changed files with 260 additions and 438 deletions
|
|
@ -18,16 +18,11 @@ package rawdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"runtime"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/prque"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitDatabaseFromFreezer reinitializes an empty database from a previous batch
|
// InitDatabaseFromFreezer reinitializes an empty database from a previous batch
|
||||||
|
|
@ -84,285 +79,6 @@ func InitDatabaseFromFreezer(db ethdb.Database) {
|
||||||
log.Info("Initialized database from freezer", "blocks", frozen, "elapsed", common.PrettyDuration(time.Since(start)))
|
log.Info("Initialized database from freezer", "blocks", frozen, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
}
|
}
|
||||||
|
|
||||||
type blockTxHashes struct {
|
|
||||||
number uint64
|
|
||||||
hashes []common.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterateTransactions iterates over all transactions in the (canon) block
|
|
||||||
// number(s) given, and yields the hashes on a channel. If there is a signal
|
|
||||||
// received from interrupt channel, the iteration will be aborted and result
|
|
||||||
// channel will be closed.
|
|
||||||
func iterateTransactions(db ethdb.Database, from uint64, to uint64, reverse bool, interrupt chan struct{}) chan *blockTxHashes {
|
|
||||||
// One thread sequentially reads data from db
|
|
||||||
type numberRlp struct {
|
|
||||||
number uint64
|
|
||||||
rlp rlp.RawValue
|
|
||||||
}
|
|
||||||
if to == from {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
threads := to - from
|
|
||||||
if cpus := runtime.NumCPU(); threads > uint64(cpus) {
|
|
||||||
threads = uint64(cpus)
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
rlpCh = make(chan *numberRlp, threads*2) // we send raw rlp over this channel
|
|
||||||
hashesCh = make(chan *blockTxHashes, threads*2) // send hashes over hashesCh
|
|
||||||
)
|
|
||||||
// lookup runs in one instance
|
|
||||||
lookup := func() {
|
|
||||||
n, end := from, to
|
|
||||||
if reverse {
|
|
||||||
n, end = to-1, from-1
|
|
||||||
}
|
|
||||||
defer close(rlpCh)
|
|
||||||
for n != end {
|
|
||||||
data := ReadCanonicalBodyRLP(db, n)
|
|
||||||
// Feed the block to the aggregator, or abort on interrupt
|
|
||||||
select {
|
|
||||||
case rlpCh <- &numberRlp{n, data}:
|
|
||||||
case <-interrupt:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if reverse {
|
|
||||||
n--
|
|
||||||
} else {
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// process runs in parallel
|
|
||||||
var nThreadsAlive atomic.Int32
|
|
||||||
nThreadsAlive.Store(int32(threads))
|
|
||||||
process := func() {
|
|
||||||
defer func() {
|
|
||||||
// Last processor closes the result channel
|
|
||||||
if nThreadsAlive.Add(-1) == 0 {
|
|
||||||
close(hashesCh)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
for data := range rlpCh {
|
|
||||||
var body types.Body
|
|
||||||
if err := rlp.DecodeBytes(data.rlp, &body); err != nil {
|
|
||||||
log.Warn("Failed to decode block body", "block", data.number, "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var hashes []common.Hash
|
|
||||||
for _, tx := range body.Transactions {
|
|
||||||
hashes = append(hashes, tx.Hash())
|
|
||||||
}
|
|
||||||
result := &blockTxHashes{
|
|
||||||
hashes: hashes,
|
|
||||||
number: data.number,
|
|
||||||
}
|
|
||||||
// Feed the block to the aggregator, or abort on interrupt
|
|
||||||
select {
|
|
||||||
case hashesCh <- result:
|
|
||||||
case <-interrupt:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go lookup() // start the sequential db accessor
|
|
||||||
for i := 0; i < int(threads); i++ {
|
|
||||||
go process()
|
|
||||||
}
|
|
||||||
return hashesCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexTransactions creates txlookup indices of the specified block range.
|
|
||||||
//
|
|
||||||
// This function iterates canonical chain in reverse order, it has one main advantage:
|
|
||||||
// We can write tx index tail flag periodically even without the whole indexing
|
|
||||||
// procedure is finished. So that we can resume indexing procedure next time quickly.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func indexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool, report bool) {
|
|
||||||
// short circuit for invalid range
|
|
||||||
if from >= to {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
hashesCh = iterateTransactions(db, from, to, true, interrupt)
|
|
||||||
batch = db.NewBatch()
|
|
||||||
start = time.Now()
|
|
||||||
logged = start.Add(-7 * time.Second)
|
|
||||||
|
|
||||||
// Since we iterate in reverse, we expect the first number to come
|
|
||||||
// in to be [to-1]. Therefore, setting lastNum to means that the
|
|
||||||
// queue gap-evaluation will work correctly
|
|
||||||
lastNum = to
|
|
||||||
queue = prque.New[int64, *blockTxHashes](nil)
|
|
||||||
blocks, txs = 0, 0 // for stats reporting
|
|
||||||
)
|
|
||||||
for chanDelivery := range hashesCh {
|
|
||||||
// Push the delivery into the queue and process contiguous ranges.
|
|
||||||
// Since we iterate in reverse, so lower numbers have lower prio, and
|
|
||||||
// we can use the number directly as prio marker
|
|
||||||
queue.Push(chanDelivery, int64(chanDelivery.number))
|
|
||||||
for !queue.Empty() {
|
|
||||||
// If the next available item is gapped, return
|
|
||||||
if _, priority := queue.Peek(); priority != int64(lastNum-1) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// For testing
|
|
||||||
if hook != nil && !hook(lastNum-1) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// Next block available, pop it off and index it
|
|
||||||
delivery := queue.PopItem()
|
|
||||||
lastNum = delivery.number
|
|
||||||
WriteTxLookupEntries(batch, delivery.number, delivery.hashes)
|
|
||||||
blocks++
|
|
||||||
txs += len(delivery.hashes)
|
|
||||||
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
|
||||||
if batch.ValueSize() > ethdb.IdealBatchSize {
|
|
||||||
WriteTxIndexTail(batch, lastNum) // Also write the tail here
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
}
|
|
||||||
// If we've spent too much time already, notify the user of what we're doing
|
|
||||||
if time.Since(logged) > 8*time.Second {
|
|
||||||
log.Info("Indexing transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flush the new indexing tail and the last committed data. It can also happen
|
|
||||||
// that the last batch is empty because nothing to index, but the tail has to
|
|
||||||
// be flushed anyway.
|
|
||||||
WriteTxIndexTail(batch, lastNum)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger := log.Debug
|
|
||||||
if report {
|
|
||||||
logger = log.Info
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-interrupt:
|
|
||||||
logger("Transaction indexing interrupted", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
default:
|
|
||||||
logger("Indexed transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// IndexTransactions creates txlookup indices of the specified block range. The from
|
|
||||||
// is included while to is excluded.
|
|
||||||
//
|
|
||||||
// This function iterates canonical chain in reverse order, it has one main advantage:
|
|
||||||
// We can write tx index tail flag periodically even without the whole indexing
|
|
||||||
// procedure is finished. So that we can resume indexing procedure next time quickly.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func IndexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, report bool) {
|
|
||||||
indexTransactions(db, from, to, interrupt, nil, report)
|
|
||||||
}
|
|
||||||
|
|
||||||
// indexTransactionsForTesting is the internal debug version with an additional hook.
|
|
||||||
func indexTransactionsForTesting(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
|
|
||||||
indexTransactions(db, from, to, interrupt, hook, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unindexTransactions removes txlookup indices of the specified block range.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func unindexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool, report bool) {
|
|
||||||
// short circuit for invalid range
|
|
||||||
if from >= to {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
hashesCh = iterateTransactions(db, from, to, false, interrupt)
|
|
||||||
batch = db.NewBatch()
|
|
||||||
start = time.Now()
|
|
||||||
logged = start.Add(-7 * time.Second)
|
|
||||||
|
|
||||||
// we expect the first number to come in to be [from]. Therefore, setting
|
|
||||||
// nextNum to from means that the queue gap-evaluation will work correctly
|
|
||||||
nextNum = from
|
|
||||||
queue = prque.New[int64, *blockTxHashes](nil)
|
|
||||||
blocks, txs = 0, 0 // for stats reporting
|
|
||||||
)
|
|
||||||
// Otherwise spin up the concurrent iterator and unindexer
|
|
||||||
for delivery := range hashesCh {
|
|
||||||
// Push the delivery into the queue and process contiguous ranges.
|
|
||||||
queue.Push(delivery, -int64(delivery.number))
|
|
||||||
for !queue.Empty() {
|
|
||||||
// If the next available item is gapped, return
|
|
||||||
if _, priority := queue.Peek(); -priority != int64(nextNum) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// For testing
|
|
||||||
if hook != nil && !hook(nextNum) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
delivery := queue.PopItem()
|
|
||||||
nextNum = delivery.number + 1
|
|
||||||
DeleteTxLookupEntries(batch, delivery.hashes)
|
|
||||||
txs += len(delivery.hashes)
|
|
||||||
blocks++
|
|
||||||
|
|
||||||
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
|
||||||
// A batch counts the size of deletion as '1', so we need to flush more
|
|
||||||
// often than that.
|
|
||||||
if blocks%1000 == 0 {
|
|
||||||
WriteTxIndexTail(batch, nextNum)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
batch.Reset()
|
|
||||||
}
|
|
||||||
// If we've spent too much time already, notify the user of what we're doing
|
|
||||||
if time.Since(logged) > 8*time.Second {
|
|
||||||
log.Info("Unindexing transactions", "blocks", blocks, "txs", txs, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
logged = time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Flush the new indexing tail and the last committed data. It can also happen
|
|
||||||
// that the last batch is empty because nothing to unindex, but the tail has to
|
|
||||||
// be flushed anyway.
|
|
||||||
WriteTxIndexTail(batch, nextNum)
|
|
||||||
if err := batch.Write(); err != nil {
|
|
||||||
log.Crit("Failed writing batch to db", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger := log.Debug
|
|
||||||
if report {
|
|
||||||
logger = log.Info
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-interrupt:
|
|
||||||
logger("Transaction unindexing interrupted", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
default:
|
|
||||||
logger("Unindexed transactions", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnindexTransactions removes txlookup indices of the specified block range.
|
|
||||||
// The from is included while to is excluded.
|
|
||||||
//
|
|
||||||
// There is a passed channel, the whole procedure will be interrupted if any
|
|
||||||
// signal received.
|
|
||||||
func UnindexTransactions(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, report bool) {
|
|
||||||
unindexTransactions(db, from, to, interrupt, nil, report)
|
|
||||||
}
|
|
||||||
|
|
||||||
// unindexTransactionsForTesting is the internal debug version with an additional hook.
|
|
||||||
func unindexTransactionsForTesting(db ethdb.Database, from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool) {
|
|
||||||
unindexTransactions(db, from, to, interrupt, hook, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PruneTransactionIndex removes all tx index entries below a certain block number.
|
// PruneTransactionIndex removes all tx index entries below a certain block number.
|
||||||
func PruneTransactionIndex(db ethdb.Database, pruneBlock uint64) {
|
func PruneTransactionIndex(db ethdb.Database, pruneBlock uint64) {
|
||||||
tail := ReadTxIndexTail(db)
|
tail := ReadTxIndexTail(db)
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ package rawdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
|
@ -28,81 +25,6 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestChainIterator(t *testing.T) {
|
|
||||||
// Construct test chain db
|
|
||||||
chainDb := NewMemoryDatabase()
|
|
||||||
|
|
||||||
var block *types.Block
|
|
||||||
var txs []*types.Transaction
|
|
||||||
to := common.BytesToAddress([]byte{0x11})
|
|
||||||
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(0))}, nil, nil, newTestHasher()) // Empty genesis block
|
|
||||||
WriteBlock(chainDb, block)
|
|
||||||
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
|
|
||||||
for i := uint64(1); i <= 10; i++ {
|
|
||||||
var tx *types.Transaction
|
|
||||||
if i%2 == 0 {
|
|
||||||
tx = types.NewTx(&types.LegacyTx{
|
|
||||||
Nonce: i,
|
|
||||||
GasPrice: big.NewInt(11111),
|
|
||||||
Gas: 1111,
|
|
||||||
To: &to,
|
|
||||||
Value: big.NewInt(111),
|
|
||||||
Data: []byte{0x11, 0x11, 0x11},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
tx = types.NewTx(&types.AccessListTx{
|
|
||||||
ChainID: big.NewInt(1337),
|
|
||||||
Nonce: i,
|
|
||||||
GasPrice: big.NewInt(11111),
|
|
||||||
Gas: 1111,
|
|
||||||
To: &to,
|
|
||||||
Value: big.NewInt(111),
|
|
||||||
Data: []byte{0x11, 0x11, 0x11},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
txs = append(txs, tx)
|
|
||||||
block = types.NewBlock(&types.Header{Number: big.NewInt(int64(i))}, &types.Body{Transactions: types.Transactions{tx}}, nil, newTestHasher())
|
|
||||||
WriteBlock(chainDb, block)
|
|
||||||
WriteCanonicalHash(chainDb, block.Hash(), block.NumberU64())
|
|
||||||
}
|
|
||||||
|
|
||||||
var cases = []struct {
|
|
||||||
from, to uint64
|
|
||||||
reverse bool
|
|
||||||
expect []int
|
|
||||||
}{
|
|
||||||
{0, 11, true, []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0}},
|
|
||||||
{0, 0, true, nil},
|
|
||||||
{0, 5, true, []int{4, 3, 2, 1, 0}},
|
|
||||||
{10, 11, true, []int{10}},
|
|
||||||
{0, 11, false, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
|
|
||||||
{0, 0, false, nil},
|
|
||||||
{10, 11, false, []int{10}},
|
|
||||||
}
|
|
||||||
for i, c := range cases {
|
|
||||||
var numbers []int
|
|
||||||
hashCh := iterateTransactions(chainDb, c.from, c.to, c.reverse, nil)
|
|
||||||
if hashCh != nil {
|
|
||||||
for h := range hashCh {
|
|
||||||
numbers = append(numbers, int(h.number))
|
|
||||||
if len(h.hashes) > 0 {
|
|
||||||
if got, exp := h.hashes[0], txs[h.number-1].Hash(); got != exp {
|
|
||||||
t.Fatalf("block %d: hash wrong, got %x exp %x", h.number, got, exp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !c.reverse {
|
|
||||||
sort.Ints(numbers)
|
|
||||||
} else {
|
|
||||||
sort.Sort(sort.Reverse(sort.IntSlice(numbers)))
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(numbers, c.expect) {
|
|
||||||
t.Fatalf("Case %d failed, visit element mismatch, want %v, got %v", i, c.expect, numbers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDatabaseWithTransactions(db ethdb.Database) ([]*types.Block, []*types.Transaction) {
|
func initDatabaseWithTransactions(db ethdb.Database) ([]*types.Block, []*types.Transaction) {
|
||||||
var blocks []*types.Block
|
var blocks []*types.Block
|
||||||
var txs []*types.Transaction
|
var txs []*types.Transaction
|
||||||
|
|
@ -147,84 +69,16 @@ func initDatabaseWithTransactions(db ethdb.Database) ([]*types.Block, []*types.T
|
||||||
return blocks, txs
|
return blocks, txs
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIndexTransactions(t *testing.T) {
|
|
||||||
// Construct test chain db
|
|
||||||
chainDB := NewMemoryDatabase()
|
|
||||||
|
|
||||||
_, txs := initDatabaseWithTransactions(chainDB)
|
|
||||||
|
|
||||||
// verify checks whether the tx indices in the range [from, to)
|
|
||||||
// is expected.
|
|
||||||
verify := func(from, to int, exist bool, tail uint64) {
|
|
||||||
for i := from; i < to; i++ {
|
|
||||||
if i == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
number := ReadTxLookupEntry(chainDB, txs[i-1].Hash())
|
|
||||||
if exist && number == nil {
|
|
||||||
t.Fatalf("Transaction index %d missing", i)
|
|
||||||
}
|
|
||||||
if !exist && number != nil {
|
|
||||||
t.Fatalf("Transaction index %d is not deleted", i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
number := ReadTxIndexTail(chainDB)
|
|
||||||
if number == nil || *number != tail {
|
|
||||||
t.Fatalf("Transaction tail mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IndexTransactions(chainDB, 5, 11, nil, false)
|
|
||||||
verify(5, 11, true, 5)
|
|
||||||
verify(0, 5, false, 5)
|
|
||||||
|
|
||||||
IndexTransactions(chainDB, 0, 5, nil, false)
|
|
||||||
verify(0, 11, true, 0)
|
|
||||||
|
|
||||||
UnindexTransactions(chainDB, 0, 5, nil, false)
|
|
||||||
verify(5, 11, true, 5)
|
|
||||||
verify(0, 5, false, 5)
|
|
||||||
|
|
||||||
UnindexTransactions(chainDB, 5, 11, nil, false)
|
|
||||||
verify(0, 11, false, 11)
|
|
||||||
|
|
||||||
// Testing corner cases
|
|
||||||
signal := make(chan struct{})
|
|
||||||
var once sync.Once
|
|
||||||
indexTransactionsForTesting(chainDB, 5, 11, signal, func(n uint64) bool {
|
|
||||||
if n <= 8 {
|
|
||||||
once.Do(func() {
|
|
||||||
close(signal)
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
verify(9, 11, true, 9)
|
|
||||||
verify(0, 9, false, 9)
|
|
||||||
IndexTransactions(chainDB, 0, 9, nil, false)
|
|
||||||
|
|
||||||
signal = make(chan struct{})
|
|
||||||
var once2 sync.Once
|
|
||||||
unindexTransactionsForTesting(chainDB, 0, 11, signal, func(n uint64) bool {
|
|
||||||
if n >= 8 {
|
|
||||||
once2.Do(func() {
|
|
||||||
close(signal)
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
verify(8, 11, true, 8)
|
|
||||||
verify(0, 8, false, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPruneTransactionIndex(t *testing.T) {
|
func TestPruneTransactionIndex(t *testing.T) {
|
||||||
chainDB := NewMemoryDatabase()
|
chainDB := NewMemoryDatabase()
|
||||||
blocks, _ := initDatabaseWithTransactions(chainDB)
|
blocks, _ := initDatabaseWithTransactions(chainDB)
|
||||||
lastBlock := blocks[len(blocks)-1].NumberU64()
|
lastBlock := blocks[len(blocks)-1].NumberU64()
|
||||||
pruneBlock := lastBlock - 3
|
pruneBlock := lastBlock - 3
|
||||||
|
|
||||||
IndexTransactions(chainDB, 0, lastBlock+1, nil, false)
|
for i := uint64(0); i <= lastBlock; i++ {
|
||||||
|
WriteTxLookupEntriesByBlock(chainDB, blocks[i])
|
||||||
|
}
|
||||||
|
WriteTxIndexTail(chainDB, 0)
|
||||||
|
|
||||||
// Check all transactions are in index.
|
// Check all transactions are in index.
|
||||||
for _, block := range blocks {
|
for _, block := range blocks {
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,17 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/common/prque"
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
"github.com/ethereum/go-ethereum/ethdb"
|
"github.com/ethereum/go-ethereum/ethdb"
|
||||||
"github.com/ethereum/go-ethereum/log"
|
"github.com/ethereum/go-ethereum/log"
|
||||||
|
"github.com/ethereum/go-ethereum/rlp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TxIndexProgress is the struct describing the progress for transaction indexing.
|
// TxIndexProgress is the struct describing the progress for transaction indexing.
|
||||||
|
|
@ -122,7 +127,7 @@ func (indexer *txIndexer) run(head uint64, stop chan struct{}, done chan struct{
|
||||||
from = head - indexer.limit + 1
|
from = head - indexer.limit + 1
|
||||||
}
|
}
|
||||||
from = max(from, indexer.cutoff)
|
from = max(from, indexer.cutoff)
|
||||||
rawdb.IndexTransactions(indexer.db, from, head+1, stop, true)
|
indexer.index(from, head+1, stop, nil, true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// The tail flag is existent (which means indexes in [tail, head] should be
|
// The tail flag is existent (which means indexes in [tail, head] should be
|
||||||
|
|
@ -130,7 +135,7 @@ func (indexer *txIndexer) run(head uint64, stop chan struct{}, done chan struct{
|
||||||
if indexer.limit == 0 || head < indexer.limit {
|
if indexer.limit == 0 || head < indexer.limit {
|
||||||
if *tail > 0 {
|
if *tail > 0 {
|
||||||
from := max(uint64(0), indexer.cutoff)
|
from := max(uint64(0), indexer.cutoff)
|
||||||
rawdb.IndexTransactions(indexer.db, from, *tail, stop, true)
|
indexer.index(from, *tail, stop, nil, true)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -140,10 +145,10 @@ func (indexer *txIndexer) run(head uint64, stop chan struct{}, done chan struct{
|
||||||
from = max(from, indexer.cutoff)
|
from = max(from, indexer.cutoff)
|
||||||
if from < *tail {
|
if from < *tail {
|
||||||
// Reindex a part of missing indices and rewind index tail to HEAD-limit
|
// Reindex a part of missing indices and rewind index tail to HEAD-limit
|
||||||
rawdb.IndexTransactions(indexer.db, from, *tail, stop, true)
|
indexer.index(from, *tail, stop, nil, true)
|
||||||
} else {
|
} else {
|
||||||
// Unindex a part of stale indices and forward index tail to HEAD-limit
|
// Unindex a part of stale indices and forward index tail to HEAD-limit
|
||||||
rawdb.UnindexTransactions(indexer.db, *tail, from, stop, false)
|
indexer.unindex(*tail, from, stop, nil, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,3 +336,250 @@ func (indexer *txIndexer) close() {
|
||||||
case <-indexer.closed:
|
case <-indexer.closed:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type blockTxHashes struct {
|
||||||
|
number uint64
|
||||||
|
hashes []common.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate iterates over all transactions in the (canon) block
|
||||||
|
// number(s) given, and yields the hashes on a channel. If there is a signal
|
||||||
|
// received from interrupt channel, the iteration will be aborted and result
|
||||||
|
// channel will be closed.
|
||||||
|
func (indexer *txIndexer) iterate(from uint64, to uint64, reverse bool, interrupt chan struct{}) chan *blockTxHashes {
|
||||||
|
// One thread sequentially reads data from db
|
||||||
|
type numberRlp struct {
|
||||||
|
number uint64
|
||||||
|
rlp rlp.RawValue
|
||||||
|
}
|
||||||
|
if to == from {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
threads := to - from
|
||||||
|
if cpus := runtime.NumCPU(); threads > uint64(cpus) {
|
||||||
|
threads = uint64(cpus)
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
rlpCh = make(chan *numberRlp, threads*2) // we send raw rlp over this channel
|
||||||
|
hashesCh = make(chan *blockTxHashes, threads*2) // send hashes over hashesCh
|
||||||
|
)
|
||||||
|
// lookup runs in one instance
|
||||||
|
lookup := func() {
|
||||||
|
n, end := from, to
|
||||||
|
if reverse {
|
||||||
|
n, end = to-1, from-1
|
||||||
|
}
|
||||||
|
defer close(rlpCh)
|
||||||
|
for n != end {
|
||||||
|
data := rawdb.ReadCanonicalBodyRLP(indexer.db, n)
|
||||||
|
// Feed the block to the aggregator, or abort on interrupt
|
||||||
|
select {
|
||||||
|
case rlpCh <- &numberRlp{n, data}:
|
||||||
|
case <-interrupt:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if reverse {
|
||||||
|
n--
|
||||||
|
} else {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// process runs in parallel
|
||||||
|
var nThreadsAlive atomic.Int32
|
||||||
|
nThreadsAlive.Store(int32(threads))
|
||||||
|
process := func() {
|
||||||
|
defer func() {
|
||||||
|
// Last processor closes the result channel
|
||||||
|
if nThreadsAlive.Add(-1) == 0 {
|
||||||
|
close(hashesCh)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for data := range rlpCh {
|
||||||
|
var body types.Body
|
||||||
|
if err := rlp.DecodeBytes(data.rlp, &body); err != nil {
|
||||||
|
log.Warn("Failed to decode block body", "block", data.number, "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var hashes []common.Hash
|
||||||
|
for _, tx := range body.Transactions {
|
||||||
|
hashes = append(hashes, tx.Hash())
|
||||||
|
}
|
||||||
|
result := &blockTxHashes{
|
||||||
|
hashes: hashes,
|
||||||
|
number: data.number,
|
||||||
|
}
|
||||||
|
// Feed the block to the aggregator, or abort on interrupt
|
||||||
|
select {
|
||||||
|
case hashesCh <- result:
|
||||||
|
case <-interrupt:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
go lookup() // start the sequential db accessor
|
||||||
|
for i := 0; i < int(threads); i++ {
|
||||||
|
go process()
|
||||||
|
}
|
||||||
|
return hashesCh
|
||||||
|
}
|
||||||
|
|
||||||
|
// index creates txlookup indices of the specified block range.
|
||||||
|
//
|
||||||
|
// This function iterates canonical chain in reverse order, it has one main advantage:
|
||||||
|
// We can write tx index tail flag periodically even without the whole indexing
|
||||||
|
// procedure is finished. So that we can resume indexing procedure next time quickly.
|
||||||
|
//
|
||||||
|
// There is a passed channel, the whole procedure will be interrupted if any
|
||||||
|
// signal received.
|
||||||
|
func (indexer *txIndexer) index(from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool, report bool) {
|
||||||
|
// short circuit for invalid range
|
||||||
|
if from >= to {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
hashesCh = indexer.iterate(from, to, true, interrupt)
|
||||||
|
batch = indexer.db.NewBatch()
|
||||||
|
start = time.Now()
|
||||||
|
logged = start.Add(-7 * time.Second)
|
||||||
|
|
||||||
|
// Since we iterate in reverse, we expect the first number to come
|
||||||
|
// in to be [to-1]. Therefore, setting lastNum to means that the
|
||||||
|
// queue gap-evaluation will work correctly
|
||||||
|
lastNum = to
|
||||||
|
queue = prque.New[int64, *blockTxHashes](nil)
|
||||||
|
blocks, txs = 0, 0 // for stats reporting
|
||||||
|
)
|
||||||
|
for chanDelivery := range hashesCh {
|
||||||
|
// Push the delivery into the queue and process contiguous ranges.
|
||||||
|
// Since we iterate in reverse, so lower numbers have lower prio, and
|
||||||
|
// we can use the number directly as prio marker
|
||||||
|
queue.Push(chanDelivery, int64(chanDelivery.number))
|
||||||
|
for !queue.Empty() {
|
||||||
|
// If the next available item is gapped, return
|
||||||
|
if _, priority := queue.Peek(); priority != int64(lastNum-1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// For testing
|
||||||
|
if hook != nil && !hook(lastNum-1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Next block available, pop it off and index it
|
||||||
|
delivery := queue.PopItem()
|
||||||
|
lastNum = delivery.number
|
||||||
|
rawdb.WriteTxLookupEntries(batch, delivery.number, delivery.hashes)
|
||||||
|
blocks++
|
||||||
|
txs += len(delivery.hashes)
|
||||||
|
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
||||||
|
if batch.ValueSize() > ethdb.IdealBatchSize {
|
||||||
|
rawdb.WriteTxIndexTail(batch, lastNum) // Also write the tail here
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
log.Crit("Failed writing batch to db", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
batch.Reset()
|
||||||
|
}
|
||||||
|
// If we've spent too much time already, notify the user of what we're doing
|
||||||
|
if time.Since(logged) > 8*time.Second {
|
||||||
|
log.Info("Indexing transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
logged = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flush the new indexing tail and the last committed data. It can also happen
|
||||||
|
// that the last batch is empty because nothing to index, but the tail has to
|
||||||
|
// be flushed anyway.
|
||||||
|
rawdb.WriteTxIndexTail(batch, lastNum)
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
log.Crit("Failed writing batch to db", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger := log.Debug
|
||||||
|
if report {
|
||||||
|
logger = log.Info
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-interrupt:
|
||||||
|
logger("Transaction indexing interrupted", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
default:
|
||||||
|
logger("Indexed transactions", "blocks", blocks, "txs", txs, "tail", lastNum, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unindex removes txlookup indices of the specified block range.
|
||||||
|
//
|
||||||
|
// There is a passed channel, the whole procedure will be interrupted if any
|
||||||
|
// signal received.
|
||||||
|
func (indexer *txIndexer) unindex(from uint64, to uint64, interrupt chan struct{}, hook func(uint64) bool, report bool) {
|
||||||
|
// short circuit for invalid range
|
||||||
|
if from >= to {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
hashesCh = indexer.iterate(from, to, false, interrupt)
|
||||||
|
batch = indexer.db.NewBatch()
|
||||||
|
start = time.Now()
|
||||||
|
logged = start.Add(-7 * time.Second)
|
||||||
|
|
||||||
|
// we expect the first number to come in to be [from]. Therefore, setting
|
||||||
|
// nextNum to from means that the queue gap-evaluation will work correctly
|
||||||
|
nextNum = from
|
||||||
|
queue = prque.New[int64, *blockTxHashes](nil)
|
||||||
|
blocks, txs = 0, 0 // for stats reporting
|
||||||
|
)
|
||||||
|
// Otherwise spin up the concurrent iterator and unindexer
|
||||||
|
for delivery := range hashesCh {
|
||||||
|
// Push the delivery into the queue and process contiguous ranges.
|
||||||
|
queue.Push(delivery, -int64(delivery.number))
|
||||||
|
for !queue.Empty() {
|
||||||
|
// If the next available item is gapped, return
|
||||||
|
if _, priority := queue.Peek(); -priority != int64(nextNum) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// For testing
|
||||||
|
if hook != nil && !hook(nextNum) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
delivery := queue.PopItem()
|
||||||
|
nextNum = delivery.number + 1
|
||||||
|
rawdb.DeleteTxLookupEntries(batch, delivery.hashes)
|
||||||
|
txs += len(delivery.hashes)
|
||||||
|
blocks++
|
||||||
|
|
||||||
|
// If enough data was accumulated in memory or we're at the last block, dump to disk
|
||||||
|
// A batch counts the size of deletion as '1', so we need to flush more
|
||||||
|
// often than that.
|
||||||
|
if blocks%1000 == 0 {
|
||||||
|
rawdb.WriteTxIndexTail(batch, nextNum)
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
log.Crit("Failed writing batch to db", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
batch.Reset()
|
||||||
|
}
|
||||||
|
// If we've spent too much time already, notify the user of what we're doing
|
||||||
|
if time.Since(logged) > 8*time.Second {
|
||||||
|
log.Info("Unindexing transactions", "blocks", blocks, "txs", txs, "total", to-from, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
logged = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Flush the new indexing tail and the last committed data. It can also happen
|
||||||
|
// that the last batch is empty because nothing to unindex, but the tail has to
|
||||||
|
// be flushed anyway.
|
||||||
|
rawdb.WriteTxIndexTail(batch, nextNum)
|
||||||
|
if err := batch.Write(); err != nil {
|
||||||
|
log.Crit("Failed writing batch to db", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger := log.Debug
|
||||||
|
if report {
|
||||||
|
logger = log.Info
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-interrupt:
|
||||||
|
logger("Transaction unindexing interrupted", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
default:
|
||||||
|
logger("Unindexed transactions", "blocks", blocks, "txs", txs, "tail", to, "elapsed", common.PrettyDuration(time.Since(start)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue