mirror of
https://github.com/luxfi/geth.git
synced 2026-10-30 15:05:39 +00:00
Callers reach for ReadAncients to read a header, a body and a receipt as one block. It took the write lock, which excludes this process's writer — but a shared reader's writer is another process, so that lock says nothing, and the name promised an atomicity it could not deliver. What such a reader can hold still is its own view, and it was not doing so. A periodic refresh landing between two of those reads answers the second from a later extent than the first, pairing a header with a body from across a move. ReadAncients now holds refreshes off until the callback returns, and a refresh that arrives meanwhile skips rather than waits: waiting would hand the caller an extent from after the read it is in the middle of, and the next read past the end asks again anyway. The store can still move underneath, which is why a read that fails because it did re-reads the index and asks once more. The test fails without the fix, seeing two extents in one read. Also proves the remaining half of the open-time cross-check. A node with an empty key-value store has no genesis to compare a store against, which looks like a hole and is not one: what it then reads IS the store's genesis, and core.LoadChainConfig refuses it against the configured one. A value that never surfaced could never be refused, so that is what the test pins, with a control showing the hash came from the store rather than the database. Co-authored-by: Hanzo Dev <[email protected]>
471 lines
14 KiB
Go
471 lines
14 KiB
Go
// Copyright 2019 The go-ethereum Authors
|
|
// This file is part of the go-ethereum library.
|
|
//
|
|
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Lesser General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public License
|
|
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package rawdb
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/gofrs/flock"
|
|
"github.com/luxfi/geth/ethdb"
|
|
"github.com/luxfi/geth/metrics"
|
|
log "github.com/luxfi/log"
|
|
)
|
|
|
|
var (
|
|
// errReadOnly is returned if the freezer is opened in read only mode. All the
|
|
// mutations are disallowed.
|
|
errReadOnly = errors.New("read only")
|
|
|
|
// errUnknownTable is returned if the user attempts to read from a table that is
|
|
// not tracked by the freezer.
|
|
errUnknownTable = errors.New("unknown table")
|
|
|
|
// errOutOrderInsertion is returned if the user attempts to inject out-of-order
|
|
// binary blobs into the freezer.
|
|
errOutOrderInsertion = errors.New("the append operation is out-order")
|
|
|
|
// errSymlinkDatadir is returned if the ancient directory specified by user
|
|
// is a symbolic link.
|
|
errSymlinkDatadir = errors.New("symbolic link datadir is not supported")
|
|
)
|
|
|
|
// freezerTableSize defines the maximum size of freezer data files.
|
|
const freezerTableSize = 2 * 1000 * 1000 * 1000
|
|
|
|
// Freezer is an append-only database to store immutable ordered data into
|
|
// flat files:
|
|
//
|
|
// - The append-only nature ensures that disk writes are minimized.
|
|
// - The in-order data ensures that disk reads are always optimized.
|
|
type Freezer struct {
|
|
datadir string
|
|
frozen atomic.Uint64 // Number of items already frozen
|
|
tail atomic.Uint64 // Number of the first stored item in the freezer
|
|
|
|
// This lock synchronizes writers and the truncate operation, as well as
|
|
// the "atomic" (batched) read operations.
|
|
writeLock sync.RWMutex
|
|
writeBatch *freezerBatch
|
|
|
|
readonly bool
|
|
tables map[string]*freezerTable // Data tables for storing everything
|
|
instanceLock *flock.Flock // File-system lock to prevent double opens
|
|
closeOnce sync.Once
|
|
|
|
// shared marks a reader that is following a writer in another process over
|
|
// the same directory. It holds no file lock and re-reads the tables as the
|
|
// writer extends them. See freezer_shared.go.
|
|
shared bool
|
|
refreshLock sync.Mutex
|
|
refreshed atomic.Int64 // unix nanos of the last pass over the tables
|
|
}
|
|
|
|
// NewFreezer creates a freezer instance for maintaining immutable ordered
|
|
// data according to the given parameters.
|
|
//
|
|
// The 'tables' argument defines the freezer tables and their configuration.
|
|
// Each value is a freezerTableConfig specifying whether snappy compression is
|
|
// disabled (noSnappy) and whether the table is prunable (prunable).
|
|
func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]freezerTableConfig) (*Freezer, error) {
|
|
// Create the initial freezer object
|
|
var (
|
|
readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil)
|
|
writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil)
|
|
sizeGauge = metrics.NewRegisteredGauge(namespace+"ancient/size", nil)
|
|
)
|
|
// Ensure the datadir is not a symbolic link if it exists.
|
|
if info, err := os.Lstat(datadir); !os.IsNotExist(err) {
|
|
if info == nil {
|
|
log.Warn("Could not Lstat the database", "path", datadir)
|
|
return nil, errors.New("lstat failed")
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
log.Warn("Symbolic link ancient database is not supported", "path", datadir)
|
|
return nil, errSymlinkDatadir
|
|
}
|
|
}
|
|
flockFile := filepath.Join(datadir, "FLOCK")
|
|
if err := os.MkdirAll(filepath.Dir(flockFile), 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
// Leveldb uses LOCK as the filelock filename. To prevent the
|
|
// name collision, we use FLOCK as the lock name.
|
|
lock := flock.New(flockFile)
|
|
tryLock := lock.TryLock
|
|
if readonly {
|
|
tryLock = lock.TryRLock
|
|
}
|
|
if locked, err := tryLock(); err != nil {
|
|
return nil, err
|
|
} else if !locked {
|
|
return nil, errors.New("locking failed")
|
|
}
|
|
// Open all the supported data tables
|
|
freezer := &Freezer{
|
|
datadir: datadir,
|
|
readonly: readonly,
|
|
tables: make(map[string]*freezerTable),
|
|
instanceLock: lock,
|
|
}
|
|
|
|
// Create the tables.
|
|
for name, config := range tables {
|
|
table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, maxTableSize, config, readonly)
|
|
if err != nil {
|
|
for _, table := range freezer.tables {
|
|
table.Close()
|
|
}
|
|
lock.Unlock()
|
|
return nil, err
|
|
}
|
|
freezer.tables[name] = table
|
|
}
|
|
var err error
|
|
if freezer.readonly {
|
|
// In readonly mode only validate, don't truncate.
|
|
// validate also sets `freezer.frozen`.
|
|
err = freezer.validate()
|
|
} else {
|
|
// Truncate all tables to common length.
|
|
err = freezer.repair()
|
|
}
|
|
if err != nil {
|
|
for _, table := range freezer.tables {
|
|
table.Close()
|
|
}
|
|
lock.Unlock()
|
|
return nil, err
|
|
}
|
|
|
|
// Create the write batch.
|
|
freezer.writeBatch = newFreezerBatch(freezer)
|
|
|
|
log.Info("Opened ancient database", "database", datadir, "readonly", readonly)
|
|
return freezer, nil
|
|
}
|
|
|
|
// Close terminates the chain freezer, closing all the data files.
|
|
func (f *Freezer) Close() error {
|
|
f.writeLock.Lock()
|
|
defer f.writeLock.Unlock()
|
|
|
|
var errs []error
|
|
f.closeOnce.Do(func() {
|
|
for _, table := range f.tables {
|
|
if err := table.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
// A shared reader never took a lock to give back.
|
|
if f.instanceLock != nil {
|
|
if err := f.instanceLock.Unlock(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
})
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
// AncientDatadir returns the path of the ancient store.
|
|
func (f *Freezer) AncientDatadir() (string, error) {
|
|
return f.datadir, nil
|
|
}
|
|
|
|
// Ancient retrieves an ancient binary blob from the append-only immutable files.
|
|
func (f *Freezer) Ancient(kind string, number uint64) ([]byte, error) {
|
|
if table := f.tables[kind]; table != nil {
|
|
if number >= f.frozen.Load() {
|
|
f.track()
|
|
}
|
|
data, err := table.Retrieve(number)
|
|
if err != nil && f.shared {
|
|
// The writer truncates the index in place, so a read that raced one
|
|
// sees a file that no longer describes what it did a moment ago.
|
|
// A private freezer's caller falls back to its own key/value copy;
|
|
// this reader deleted that copy precisely because the store holds
|
|
// the block, so failing here would report it missing. Re-read the
|
|
// index and ask once more.
|
|
if rerr := table.refresh(); rerr == nil {
|
|
return table.Retrieve(number)
|
|
}
|
|
}
|
|
return data, err
|
|
}
|
|
return nil, errUnknownTable
|
|
}
|
|
|
|
// AncientRange retrieves multiple items in sequence, starting from the index 'start'.
|
|
// It will return
|
|
// - at most 'count' items,
|
|
// - if maxBytes is specified: at least 1 item (even if exceeding the maxByteSize),
|
|
// but will otherwise return as many items as fit into maxByteSize.
|
|
// - if maxBytes is not specified, 'count' items will be returned if they are present.
|
|
func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
|
|
if table := f.tables[kind]; table != nil {
|
|
if start+count > f.frozen.Load() {
|
|
f.track()
|
|
}
|
|
return table.RetrieveItems(start, count, maxBytes)
|
|
}
|
|
return nil, errUnknownTable
|
|
}
|
|
|
|
// AncientBytes retrieves the value segment of the element specified by the id
|
|
// and value offsets.
|
|
func (f *Freezer) AncientBytes(kind string, id, offset, length uint64) ([]byte, error) {
|
|
if table := f.tables[kind]; table != nil {
|
|
if id >= f.frozen.Load() {
|
|
f.track()
|
|
}
|
|
return table.RetrieveBytes(id, offset, length)
|
|
}
|
|
return nil, errUnknownTable
|
|
}
|
|
|
|
// Ancients returns the length of the frozen items.
|
|
func (f *Freezer) Ancients() (uint64, error) {
|
|
f.track()
|
|
return f.frozen.Load(), nil
|
|
}
|
|
|
|
// Tail returns the number of first stored item in the freezer.
|
|
func (f *Freezer) Tail() (uint64, error) {
|
|
f.track()
|
|
return f.tail.Load(), nil
|
|
}
|
|
|
|
// AncientSize returns the ancient size of the specified category.
|
|
func (f *Freezer) AncientSize(kind string) (uint64, error) {
|
|
// This needs the write lock to avoid data races on table fields.
|
|
// Speed doesn't matter here, AncientSize is for debugging.
|
|
f.writeLock.RLock()
|
|
defer f.writeLock.RUnlock()
|
|
|
|
if table := f.tables[kind]; table != nil {
|
|
return table.size()
|
|
}
|
|
return 0, errUnknownTable
|
|
}
|
|
|
|
// ReadAncients runs the given read operation while ensuring that no writes take place
|
|
// on the underlying freezer.
|
|
//
|
|
// A shared reader qualifies that promise, because the writer is another process
|
|
// and this lock says nothing about it. What such a reader can hold still is its
|
|
// own view. Callers read several tables here and treat the answers as one block,
|
|
// so a refresh landing between two of them would answer from two different
|
|
// extents; refreshes are held off until the callback returns. The store itself
|
|
// can still move underneath, which is why a read that fails because it did
|
|
// re-reads the index and asks again.
|
|
func (f *Freezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
|
|
f.writeLock.RLock()
|
|
defer f.writeLock.RUnlock()
|
|
|
|
if f.shared {
|
|
f.refreshLock.Lock()
|
|
defer f.refreshLock.Unlock()
|
|
}
|
|
return fn(f)
|
|
}
|
|
|
|
// ModifyAncients runs the given write operation.
|
|
func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
|
|
if f.readonly {
|
|
return 0, errReadOnly
|
|
}
|
|
f.writeLock.Lock()
|
|
defer f.writeLock.Unlock()
|
|
|
|
// Roll back all tables to the starting position in case of error.
|
|
prevItem := f.frozen.Load()
|
|
defer func() {
|
|
if err != nil {
|
|
// The write operation has failed. Go back to the previous item position.
|
|
for name, table := range f.tables {
|
|
err := table.truncateHead(prevItem)
|
|
if err != nil {
|
|
log.Error("Freezer table roll-back failed", "table", name, "index", prevItem, "err", err)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
f.writeBatch.reset()
|
|
if err := fn(f.writeBatch); err != nil {
|
|
return 0, err
|
|
}
|
|
item, writeSize, err := f.writeBatch.commit()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
f.frozen.Store(item)
|
|
return writeSize, nil
|
|
}
|
|
|
|
// TruncateHead discards any recent data above the provided threshold number.
|
|
// It returns the previous head number.
|
|
func (f *Freezer) TruncateHead(items uint64) (uint64, error) {
|
|
if f.readonly {
|
|
return 0, errReadOnly
|
|
}
|
|
f.writeLock.Lock()
|
|
defer f.writeLock.Unlock()
|
|
|
|
oitems := f.frozen.Load()
|
|
if oitems <= items {
|
|
return oitems, nil
|
|
}
|
|
for _, table := range f.tables {
|
|
if err := table.truncateHead(items); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
f.frozen.Store(items)
|
|
return oitems, nil
|
|
}
|
|
|
|
// TruncateTail discards all data below the specified threshold. Note that only
|
|
// 'prunable' tables will be truncated.
|
|
func (f *Freezer) TruncateTail(tail uint64) (uint64, error) {
|
|
if f.readonly {
|
|
return 0, errReadOnly
|
|
}
|
|
f.writeLock.Lock()
|
|
defer f.writeLock.Unlock()
|
|
|
|
old := f.tail.Load()
|
|
if old >= tail {
|
|
return old, nil
|
|
}
|
|
for _, table := range f.tables {
|
|
if table.config.prunable {
|
|
if err := table.truncateTail(tail); err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
f.tail.Store(tail)
|
|
return old, nil
|
|
}
|
|
|
|
// SyncAncient flushes all data tables to disk.
|
|
func (f *Freezer) SyncAncient() error {
|
|
if f.readonly {
|
|
// A reader buffers nothing, so there is nothing to flush, and its file
|
|
// handles cannot be synced anyway.
|
|
return nil
|
|
}
|
|
var errs []error
|
|
for _, table := range f.tables {
|
|
if err := table.Sync(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if errs != nil {
|
|
return fmt.Errorf("%v", errs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validate checks that every table has the same boundary.
|
|
// Used instead of `repair` in readonly mode.
|
|
func (f *Freezer) validate() error {
|
|
if len(f.tables) == 0 {
|
|
return nil
|
|
}
|
|
var (
|
|
head uint64
|
|
prunedTail *uint64
|
|
)
|
|
// get any head value
|
|
for _, table := range f.tables {
|
|
head = table.items.Load()
|
|
break
|
|
}
|
|
for kind, table := range f.tables {
|
|
// all tables have to have the same head
|
|
if head != table.items.Load() {
|
|
return fmt.Errorf("freezer table %s has a differing head: %d != %d", kind, table.items.Load(), head)
|
|
}
|
|
if !table.config.prunable {
|
|
// non-prunable tables have to start at 0
|
|
if table.itemHidden.Load() != 0 {
|
|
return fmt.Errorf("non-prunable freezer table '%s' has a non-zero tail: %d", kind, table.itemHidden.Load())
|
|
}
|
|
} else {
|
|
// prunable tables have to have the same length
|
|
if prunedTail == nil {
|
|
tmp := table.itemHidden.Load()
|
|
prunedTail = &tmp
|
|
}
|
|
if *prunedTail != table.itemHidden.Load() {
|
|
return fmt.Errorf("freezer table %s has differing tail: %d != %d", kind, table.itemHidden.Load(), *prunedTail)
|
|
}
|
|
}
|
|
}
|
|
|
|
if prunedTail == nil {
|
|
tmp := uint64(0)
|
|
prunedTail = &tmp
|
|
}
|
|
|
|
f.frozen.Store(head)
|
|
f.tail.Store(*prunedTail)
|
|
return nil
|
|
}
|
|
|
|
// repair truncates all data tables to the same length.
|
|
func (f *Freezer) repair() error {
|
|
var (
|
|
head = uint64(math.MaxUint64)
|
|
prunedTail = uint64(0)
|
|
)
|
|
// get the minimal head and the maximum tail
|
|
for _, table := range f.tables {
|
|
head = min(head, table.items.Load())
|
|
prunedTail = max(prunedTail, table.itemHidden.Load())
|
|
}
|
|
// apply the pruning
|
|
for kind, table := range f.tables {
|
|
// all tables need to have the same head
|
|
if err := table.truncateHead(head); err != nil {
|
|
return err
|
|
}
|
|
if !table.config.prunable {
|
|
// non-prunable tables have to start at 0
|
|
if table.itemHidden.Load() != 0 {
|
|
panic(fmt.Sprintf("non-prunable freezer table %s has non-zero tail: %v", kind, table.itemHidden.Load()))
|
|
}
|
|
} else {
|
|
// prunable tables have to have the same length
|
|
if err := table.truncateTail(prunedTail); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
f.frozen.Store(head)
|
|
f.tail.Store(prunedTail)
|
|
return nil
|
|
}
|