github.com/tacshi/go-ethereum@v0.0.0-20230616113857-84a434e20921/core/rawdb/freezer.go (about)

     1  // Copyright 2019 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package rawdb
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"math"
    23  	"os"
    24  	"path/filepath"
    25  	"sync"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"github.com/tacshi/go-ethereum/common"
    30  	"github.com/tacshi/go-ethereum/ethdb"
    31  	"github.com/tacshi/go-ethereum/log"
    32  	"github.com/tacshi/go-ethereum/metrics"
    33  )
    34  
    35  var (
    36  	// errReadOnly is returned if the freezer is opened in read only mode. All the
    37  	// mutations are disallowed.
    38  	errReadOnly = errors.New("read only")
    39  
    40  	// errUnknownTable is returned if the user attempts to read from a table that is
    41  	// not tracked by the freezer.
    42  	errUnknownTable = errors.New("unknown table")
    43  
    44  	// errOutOrderInsertion is returned if the user attempts to inject out-of-order
    45  	// binary blobs into the freezer.
    46  	errOutOrderInsertion = errors.New("the append operation is out-order")
    47  
    48  	// errSymlinkDatadir is returned if the ancient directory specified by user
    49  	// is a symbolic link.
    50  	errSymlinkDatadir = errors.New("symbolic link datadir is not supported")
    51  )
    52  
    53  // freezerTableSize defines the maximum size of freezer data files.
    54  const freezerTableSize = 2 * 1000 * 1000 * 1000
    55  
    56  // Freezer is a memory mapped append-only database to store immutable ordered
    57  // data into flat files:
    58  //
    59  //   - The append-only nature ensures that disk writes are minimized.
    60  //   - The memory mapping ensures we can max out system memory for caching without
    61  //     reserving it for go-ethereum. This would also reduce the memory requirements
    62  //     of Geth, and thus also GC overhead.
    63  type Freezer struct {
    64  	// WARNING: The `frozen` and `tail` fields are accessed atomically. On 32 bit platforms, only
    65  	// 64-bit aligned fields can be atomic. The struct is guaranteed to be so aligned,
    66  	// so take advantage of that (https://golang.org/pkg/sync/atomic/#pkg-note-BUG).
    67  	frozen uint64 // Number of blocks already frozen
    68  	tail   uint64 // Number of the first stored item in the freezer
    69  
    70  	// This lock synchronizes writers and the truncate operation, as well as
    71  	// the "atomic" (batched) read operations.
    72  	writeLock  sync.RWMutex
    73  	writeBatch *freezerBatch
    74  
    75  	readonly     bool
    76  	tables       map[string]*freezerTable // Data tables for storing everything
    77  	instanceLock FileLock                 // File-system lock to prevent double opens
    78  	closeOnce    sync.Once
    79  }
    80  
    81  // NewChainFreezer is a small utility method around NewFreezer that sets the
    82  // default parameters for the chain storage.
    83  func NewChainFreezer(datadir string, namespace string, readonly bool) (*Freezer, error) {
    84  	return NewFreezer(datadir, namespace, readonly, freezerTableSize, chainFreezerNoSnappy)
    85  }
    86  
    87  // NewFreezer creates a freezer instance for maintaining immutable ordered
    88  // data according to the given parameters.
    89  //
    90  // The 'tables' argument defines the data tables. If the value of a map
    91  // entry is true, snappy compression is disabled for the table.
    92  func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*Freezer, error) {
    93  	// Create the initial freezer object
    94  	var (
    95  		readMeter  = metrics.NewRegisteredMeter(namespace+"ancient/read", nil)
    96  		writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil)
    97  		sizeGauge  = metrics.NewRegisteredGauge(namespace+"ancient/size", nil)
    98  	)
    99  	// Ensure the datadir is not a symbolic link if it exists.
   100  	if info, err := os.Lstat(datadir); !os.IsNotExist(err) {
   101  		if info.Mode()&os.ModeSymlink != 0 {
   102  			log.Warn("Symbolic link ancient database is not supported", "path", datadir)
   103  			return nil, errSymlinkDatadir
   104  		}
   105  	}
   106  	flockFile := filepath.Join(datadir, "FLOCK")
   107  	if err := os.MkdirAll(filepath.Dir(flockFile), 0755); err != nil {
   108  		return nil, err
   109  	}
   110  	// Leveldb uses LOCK as the filelock filename. To prevent the
   111  	// name collision, we use FLOCK as the lock name.
   112  	lock := NewFileLock(flockFile)
   113  	if locked, err := lock.TryLock(); err != nil {
   114  		return nil, err
   115  	} else if !locked {
   116  		return nil, errors.New("locking failed")
   117  	}
   118  	// Open all the supported data tables
   119  	freezer := &Freezer{
   120  		readonly:     readonly,
   121  		tables:       make(map[string]*freezerTable),
   122  		instanceLock: lock,
   123  	}
   124  
   125  	// Create the tables.
   126  	for name, disableSnappy := range tables {
   127  		table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, maxTableSize, disableSnappy, readonly)
   128  		if err != nil {
   129  			for _, table := range freezer.tables {
   130  				table.Close()
   131  			}
   132  			lock.Unlock()
   133  			return nil, err
   134  		}
   135  		freezer.tables[name] = table
   136  	}
   137  	var err error
   138  	if freezer.readonly {
   139  		// In readonly mode only validate, don't truncate.
   140  		// validate also sets `freezer.frozen`.
   141  		err = freezer.validate()
   142  	} else {
   143  		// Truncate all tables to common length.
   144  		err = freezer.repair()
   145  	}
   146  	if err != nil {
   147  		for _, table := range freezer.tables {
   148  			table.Close()
   149  		}
   150  		lock.Unlock()
   151  		return nil, err
   152  	}
   153  
   154  	// Create the write batch.
   155  	freezer.writeBatch = newFreezerBatch(freezer)
   156  
   157  	log.Info("Opened ancient database", "database", datadir, "readonly", readonly)
   158  	return freezer, nil
   159  }
   160  
   161  // Close terminates the chain freezer, unmapping all the data files.
   162  func (f *Freezer) Close() error {
   163  	f.writeLock.Lock()
   164  	defer f.writeLock.Unlock()
   165  
   166  	var errs []error
   167  	f.closeOnce.Do(func() {
   168  		for _, table := range f.tables {
   169  			if err := table.Close(); err != nil {
   170  				errs = append(errs, err)
   171  			}
   172  		}
   173  		if err := f.instanceLock.Unlock(); err != nil {
   174  			errs = append(errs, err)
   175  		}
   176  	})
   177  	if errs != nil {
   178  		return fmt.Errorf("%v", errs)
   179  	}
   180  	return nil
   181  }
   182  
   183  // HasAncient returns an indicator whether the specified ancient data exists
   184  // in the freezer.
   185  func (f *Freezer) HasAncient(kind string, number uint64) (bool, error) {
   186  	if table := f.tables[kind]; table != nil {
   187  		return table.has(number), nil
   188  	}
   189  	return false, nil
   190  }
   191  
   192  // Ancient retrieves an ancient binary blob from the append-only immutable files.
   193  func (f *Freezer) Ancient(kind string, number uint64) ([]byte, error) {
   194  	if table := f.tables[kind]; table != nil {
   195  		return table.Retrieve(number)
   196  	}
   197  	return nil, errUnknownTable
   198  }
   199  
   200  // AncientRange retrieves multiple items in sequence, starting from the index 'start'.
   201  // It will return
   202  //   - at most 'max' items,
   203  //   - at least 1 item (even if exceeding the maxByteSize), but will otherwise
   204  //     return as many items as fit into maxByteSize.
   205  func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
   206  	if table := f.tables[kind]; table != nil {
   207  		return table.RetrieveItems(start, count, maxBytes)
   208  	}
   209  	return nil, errUnknownTable
   210  }
   211  
   212  // Ancients returns the length of the frozen items.
   213  func (f *Freezer) Ancients() (uint64, error) {
   214  	return atomic.LoadUint64(&f.frozen), nil
   215  }
   216  
   217  // Tail returns the number of first stored item in the freezer.
   218  func (f *Freezer) Tail() (uint64, error) {
   219  	return atomic.LoadUint64(&f.tail), nil
   220  }
   221  
   222  // AncientSize returns the ancient size of the specified category.
   223  func (f *Freezer) AncientSize(kind string) (uint64, error) {
   224  	// This needs the write lock to avoid data races on table fields.
   225  	// Speed doesn't matter here, AncientSize is for debugging.
   226  	f.writeLock.RLock()
   227  	defer f.writeLock.RUnlock()
   228  
   229  	if table := f.tables[kind]; table != nil {
   230  		return table.size()
   231  	}
   232  	return 0, errUnknownTable
   233  }
   234  
   235  // ReadAncients runs the given read operation while ensuring that no writes take place
   236  // on the underlying freezer.
   237  func (f *Freezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
   238  	f.writeLock.RLock()
   239  	defer f.writeLock.RUnlock()
   240  
   241  	return fn(f)
   242  }
   243  
   244  // ModifyAncients runs the given write operation.
   245  func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
   246  	if f.readonly {
   247  		return 0, errReadOnly
   248  	}
   249  	f.writeLock.Lock()
   250  	defer f.writeLock.Unlock()
   251  
   252  	// Roll back all tables to the starting position in case of error.
   253  	prevItem := atomic.LoadUint64(&f.frozen)
   254  	defer func() {
   255  		if err != nil {
   256  			// The write operation has failed. Go back to the previous item position.
   257  			for name, table := range f.tables {
   258  				err := table.truncateHead(prevItem)
   259  				if err != nil {
   260  					log.Error("Freezer table roll-back failed", "table", name, "index", prevItem, "err", err)
   261  				}
   262  			}
   263  		}
   264  	}()
   265  
   266  	f.writeBatch.reset()
   267  	if err := fn(f.writeBatch); err != nil {
   268  		return 0, err
   269  	}
   270  	item, writeSize, err := f.writeBatch.commit()
   271  	if err != nil {
   272  		return 0, err
   273  	}
   274  	atomic.StoreUint64(&f.frozen, item)
   275  	return writeSize, nil
   276  }
   277  
   278  // TruncateHead discards any recent data above the provided threshold number.
   279  func (f *Freezer) TruncateHead(items uint64) error {
   280  	if f.readonly {
   281  		return errReadOnly
   282  	}
   283  	f.writeLock.Lock()
   284  	defer f.writeLock.Unlock()
   285  
   286  	if atomic.LoadUint64(&f.frozen) <= items {
   287  		return nil
   288  	}
   289  	for _, table := range f.tables {
   290  		if err := table.truncateHead(items); err != nil {
   291  			return err
   292  		}
   293  	}
   294  	atomic.StoreUint64(&f.frozen, items)
   295  	return nil
   296  }
   297  
   298  // TruncateTail discards any recent data below the provided threshold number.
   299  func (f *Freezer) TruncateTail(tail uint64) error {
   300  	if f.readonly {
   301  		return errReadOnly
   302  	}
   303  	f.writeLock.Lock()
   304  	defer f.writeLock.Unlock()
   305  
   306  	if atomic.LoadUint64(&f.tail) >= tail {
   307  		return nil
   308  	}
   309  	for _, table := range f.tables {
   310  		if err := table.truncateTail(tail); err != nil {
   311  			return err
   312  		}
   313  	}
   314  	atomic.StoreUint64(&f.tail, tail)
   315  	return nil
   316  }
   317  
   318  // Sync flushes all data tables to disk.
   319  func (f *Freezer) Sync() error {
   320  	var errs []error
   321  	for _, table := range f.tables {
   322  		if err := table.Sync(); err != nil {
   323  			errs = append(errs, err)
   324  		}
   325  	}
   326  	if errs != nil {
   327  		return fmt.Errorf("%v", errs)
   328  	}
   329  	return nil
   330  }
   331  
   332  // validate checks that every table has the same boundary.
   333  // Used instead of `repair` in readonly mode.
   334  func (f *Freezer) validate() error {
   335  	if len(f.tables) == 0 {
   336  		return nil
   337  	}
   338  	var (
   339  		head uint64
   340  		tail uint64
   341  		name string
   342  	)
   343  	// Hack to get boundary of any table
   344  	for kind, table := range f.tables {
   345  		head = atomic.LoadUint64(&table.items)
   346  		tail = atomic.LoadUint64(&table.itemHidden)
   347  		name = kind
   348  		break
   349  	}
   350  	// Now check every table against those boundaries.
   351  	for kind, table := range f.tables {
   352  		if head != atomic.LoadUint64(&table.items) {
   353  			return fmt.Errorf("freezer tables %s and %s have differing head: %d != %d", kind, name, atomic.LoadUint64(&table.items), head)
   354  		}
   355  		if tail != atomic.LoadUint64(&table.itemHidden) {
   356  			return fmt.Errorf("freezer tables %s and %s have differing tail: %d != %d", kind, name, atomic.LoadUint64(&table.itemHidden), tail)
   357  		}
   358  	}
   359  	atomic.StoreUint64(&f.frozen, head)
   360  	atomic.StoreUint64(&f.tail, tail)
   361  	return nil
   362  }
   363  
   364  // repair truncates all data tables to the same length.
   365  func (f *Freezer) repair() error {
   366  	var (
   367  		head = uint64(math.MaxUint64)
   368  		tail = uint64(0)
   369  	)
   370  	for _, table := range f.tables {
   371  		items := atomic.LoadUint64(&table.items)
   372  		if head > items {
   373  			head = items
   374  		}
   375  		hidden := atomic.LoadUint64(&table.itemHidden)
   376  		if hidden > tail {
   377  			tail = hidden
   378  		}
   379  	}
   380  	for _, table := range f.tables {
   381  		if err := table.truncateHead(head); err != nil {
   382  			return err
   383  		}
   384  		if err := table.truncateTail(tail); err != nil {
   385  			return err
   386  		}
   387  	}
   388  	atomic.StoreUint64(&f.frozen, head)
   389  	atomic.StoreUint64(&f.tail, tail)
   390  	return nil
   391  }
   392  
   393  // convertLegacyFn takes a raw freezer entry in an older format and
   394  // returns it in the new format.
   395  type convertLegacyFn = func([]byte) ([]byte, error)
   396  
   397  // MigrateTable processes the entries in a given table in sequence
   398  // converting them to a new format if they're of an old format.
   399  func (f *Freezer) MigrateTable(kind string, convert convertLegacyFn) error {
   400  	if f.readonly {
   401  		return errReadOnly
   402  	}
   403  	f.writeLock.Lock()
   404  	defer f.writeLock.Unlock()
   405  
   406  	table, ok := f.tables[kind]
   407  	if !ok {
   408  		return errUnknownTable
   409  	}
   410  	// forEach iterates every entry in the table serially and in order, calling `fn`
   411  	// with the item as argument. If `fn` returns an error the iteration stops
   412  	// and that error will be returned.
   413  	forEach := func(t *freezerTable, offset uint64, fn func(uint64, []byte) error) error {
   414  		var (
   415  			items     = atomic.LoadUint64(&t.items)
   416  			batchSize = uint64(1024)
   417  			maxBytes  = uint64(1024 * 1024)
   418  		)
   419  		for i := offset; i < items; {
   420  			if i+batchSize > items {
   421  				batchSize = items - i
   422  			}
   423  			data, err := t.RetrieveItems(i, batchSize, maxBytes)
   424  			if err != nil {
   425  				return err
   426  			}
   427  			for j, item := range data {
   428  				if err := fn(i+uint64(j), item); err != nil {
   429  					return err
   430  				}
   431  			}
   432  			i += uint64(len(data))
   433  		}
   434  		return nil
   435  	}
   436  	// TODO(s1na): This is a sanity-check since as of now no process does tail-deletion. But the migration
   437  	// process assumes no deletion at tail and needs to be modified to account for that.
   438  	if table.itemOffset > 0 || table.itemHidden > 0 {
   439  		return fmt.Errorf("migration not supported for tail-deleted freezers")
   440  	}
   441  	ancientsPath := filepath.Dir(table.index.Name())
   442  	// Set up new dir for the migrated table, the content of which
   443  	// we'll at the end move over to the ancients dir.
   444  	migrationPath := filepath.Join(ancientsPath, "migration")
   445  	newTable, err := newFreezerTable(migrationPath, kind, table.noCompression, false)
   446  	if err != nil {
   447  		return err
   448  	}
   449  	var (
   450  		batch  = newTable.newBatch()
   451  		out    []byte
   452  		start  = time.Now()
   453  		logged = time.Now()
   454  		offset = newTable.items
   455  	)
   456  	if offset > 0 {
   457  		log.Info("found previous migration attempt", "migrated", offset)
   458  	}
   459  	// Iterate through entries and transform them
   460  	if err := forEach(table, offset, func(i uint64, blob []byte) error {
   461  		if i%10000 == 0 && time.Since(logged) > 16*time.Second {
   462  			log.Info("Processing legacy elements", "count", i, "elapsed", common.PrettyDuration(time.Since(start)))
   463  			logged = time.Now()
   464  		}
   465  		out, err = convert(blob)
   466  		if err != nil {
   467  			return err
   468  		}
   469  		if err := batch.AppendRaw(i, out); err != nil {
   470  			return err
   471  		}
   472  		return nil
   473  	}); err != nil {
   474  		return err
   475  	}
   476  	if err := batch.commit(); err != nil {
   477  		return err
   478  	}
   479  	log.Info("Replacing old table files with migrated ones", "elapsed", common.PrettyDuration(time.Since(start)))
   480  	// Release and delete old table files. Note this won't
   481  	// delete the index file.
   482  	table.releaseFilesAfter(0, true)
   483  
   484  	if err := newTable.Close(); err != nil {
   485  		return err
   486  	}
   487  	files, err := os.ReadDir(migrationPath)
   488  	if err != nil {
   489  		return err
   490  	}
   491  	// Move migrated files to ancients dir.
   492  	for _, f := range files {
   493  		// This will replace the old index file as a side-effect.
   494  		if err := os.Rename(filepath.Join(migrationPath, f.Name()), filepath.Join(ancientsPath, f.Name())); err != nil {
   495  			return err
   496  		}
   497  	}
   498  	// Delete by now empty dir.
   499  	if err := os.Remove(migrationPath); err != nil {
   500  		return err
   501  	}
   502  	return nil
   503  }