github.com/ethereum-optimism/optimism/l2geth@v0.0.0-20230612200230-50b04ade19e3/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/atomic" 26 "time" 27 28 "github.com/ethereum-optimism/optimism/l2geth/common" 29 "github.com/ethereum-optimism/optimism/l2geth/ethdb" 30 "github.com/ethereum-optimism/optimism/l2geth/log" 31 "github.com/ethereum-optimism/optimism/l2geth/metrics" 32 "github.com/ethereum-optimism/optimism/l2geth/params" 33 "github.com/prometheus/tsdb/fileutil" 34 ) 35 36 var ( 37 // errUnknownTable is returned if the user attempts to read from a table that is 38 // not tracked by the freezer. 39 errUnknownTable = errors.New("unknown table") 40 41 // errOutOrderInsertion is returned if the user attempts to inject out-of-order 42 // binary blobs into the freezer. 43 errOutOrderInsertion = errors.New("the append operation is out-order") 44 45 // errSymlinkDatadir is returned if the ancient directory specified by user 46 // is a symbolic link. 47 errSymlinkDatadir = errors.New("symbolic link datadir is not supported") 48 ) 49 50 const ( 51 // freezerRecheckInterval is the frequency to check the key-value database for 52 // chain progression that might permit new blocks to be frozen into immutable 53 // storage. 54 freezerRecheckInterval = time.Minute 55 56 // freezerBatchLimit is the maximum number of blocks to freeze in one batch 57 // before doing an fsync and deleting it from the key-value store. 58 freezerBatchLimit = 30000 59 ) 60 61 // freezer is an memory mapped append-only database to store immutable chain data 62 // into flat files: 63 // 64 // - The append only nature ensures that disk writes are minimized. 65 // - The memory mapping ensures we can max out system memory for caching without 66 // reserving it for go-ethereum. This would also reduce the memory requirements 67 // of Geth, and thus also GC overhead. 68 type freezer struct { 69 // WARNING: The `frozen` field is accessed atomically. On 32 bit platforms, only 70 // 64-bit aligned fields can be atomic. The struct is guaranteed to be so aligned, 71 // so take advantage of that (https://golang.org/pkg/sync/atomic/#pkg-note-BUG). 72 frozen uint64 // Number of blocks already frozen 73 74 tables map[string]*freezerTable // Data tables for storing everything 75 instanceLock fileutil.Releaser // File-system lock to prevent double opens 76 } 77 78 // newFreezer creates a chain freezer that moves ancient chain data into 79 // append-only flat file containers. 80 func newFreezer(datadir string, namespace string) (*freezer, error) { 81 // Create the initial freezer object 82 var ( 83 readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) 84 writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) 85 sizeGauge = metrics.NewRegisteredGauge(namespace+"ancient/size", nil) 86 ) 87 // Ensure the datadir is not a symbolic link if it exists. 88 if info, err := os.Lstat(datadir); !os.IsNotExist(err) { 89 if info.Mode()&os.ModeSymlink != 0 { 90 log.Warn("Symbolic link ancient database is not supported", "path", datadir) 91 return nil, errSymlinkDatadir 92 } 93 } 94 // Leveldb uses LOCK as the filelock filename. To prevent the 95 // name collision, we use FLOCK as the lock name. 96 lock, _, err := fileutil.Flock(filepath.Join(datadir, "FLOCK")) 97 if err != nil { 98 return nil, err 99 } 100 // Open all the supported data tables 101 freezer := &freezer{ 102 tables: make(map[string]*freezerTable), 103 instanceLock: lock, 104 } 105 for name, disableSnappy := range freezerNoSnappy { 106 table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, disableSnappy) 107 if err != nil { 108 for _, table := range freezer.tables { 109 table.Close() 110 } 111 lock.Release() 112 return nil, err 113 } 114 freezer.tables[name] = table 115 } 116 if err := freezer.repair(); err != nil { 117 for _, table := range freezer.tables { 118 table.Close() 119 } 120 lock.Release() 121 return nil, err 122 } 123 log.Info("Opened ancient database", "database", datadir) 124 return freezer, nil 125 } 126 127 // Close terminates the chain freezer, unmapping all the data files. 128 func (f *freezer) Close() error { 129 var errs []error 130 for _, table := range f.tables { 131 if err := table.Close(); err != nil { 132 errs = append(errs, err) 133 } 134 } 135 if err := f.instanceLock.Release(); err != nil { 136 errs = append(errs, err) 137 } 138 if errs != nil { 139 return fmt.Errorf("%v", errs) 140 } 141 return nil 142 } 143 144 // HasAncient returns an indicator whether the specified ancient data exists 145 // in the freezer. 146 func (f *freezer) HasAncient(kind string, number uint64) (bool, error) { 147 if table := f.tables[kind]; table != nil { 148 return table.has(number), nil 149 } 150 return false, nil 151 } 152 153 // Ancient retrieves an ancient binary blob from the append-only immutable files. 154 func (f *freezer) Ancient(kind string, number uint64) ([]byte, error) { 155 if table := f.tables[kind]; table != nil { 156 return table.Retrieve(number) 157 } 158 return nil, errUnknownTable 159 } 160 161 // Ancients returns the length of the frozen items. 162 func (f *freezer) Ancients() (uint64, error) { 163 return atomic.LoadUint64(&f.frozen), nil 164 } 165 166 // AncientSize returns the ancient size of the specified category. 167 func (f *freezer) AncientSize(kind string) (uint64, error) { 168 if table := f.tables[kind]; table != nil { 169 return table.size() 170 } 171 return 0, errUnknownTable 172 } 173 174 // AppendAncient injects all binary blobs belong to block at the end of the 175 // append-only immutable table files. 176 // 177 // Notably, this function is lock free but kind of thread-safe. All out-of-order 178 // injection will be rejected. But if two injections with same number happen at 179 // the same time, we can get into the trouble. 180 func (f *freezer) AppendAncient(number uint64, hash, header, body, receipts, td []byte) (err error) { 181 // Ensure the binary blobs we are appending is continuous with freezer. 182 if atomic.LoadUint64(&f.frozen) != number { 183 return errOutOrderInsertion 184 } 185 // Rollback all inserted data if any insertion below failed to ensure 186 // the tables won't out of sync. 187 defer func() { 188 if err != nil { 189 rerr := f.repair() 190 if rerr != nil { 191 log.Crit("Failed to repair freezer", "err", rerr) 192 } 193 log.Info("Append ancient failed", "number", number, "err", err) 194 } 195 }() 196 // Inject all the components into the relevant data tables 197 if err := f.tables[freezerHashTable].Append(f.frozen, hash[:]); err != nil { 198 log.Error("Failed to append ancient hash", "number", f.frozen, "hash", hash, "err", err) 199 return err 200 } 201 if err := f.tables[freezerHeaderTable].Append(f.frozen, header); err != nil { 202 log.Error("Failed to append ancient header", "number", f.frozen, "hash", hash, "err", err) 203 return err 204 } 205 if err := f.tables[freezerBodiesTable].Append(f.frozen, body); err != nil { 206 log.Error("Failed to append ancient body", "number", f.frozen, "hash", hash, "err", err) 207 return err 208 } 209 if err := f.tables[freezerReceiptTable].Append(f.frozen, receipts); err != nil { 210 log.Error("Failed to append ancient receipts", "number", f.frozen, "hash", hash, "err", err) 211 return err 212 } 213 if err := f.tables[freezerDifficultyTable].Append(f.frozen, td); err != nil { 214 log.Error("Failed to append ancient difficulty", "number", f.frozen, "hash", hash, "err", err) 215 return err 216 } 217 atomic.AddUint64(&f.frozen, 1) // Only modify atomically 218 return nil 219 } 220 221 // Truncate discards any recent data above the provided threshold number. 222 func (f *freezer) TruncateAncients(items uint64) error { 223 if atomic.LoadUint64(&f.frozen) <= items { 224 return nil 225 } 226 for _, table := range f.tables { 227 if err := table.truncate(items); err != nil { 228 return err 229 } 230 } 231 atomic.StoreUint64(&f.frozen, items) 232 return nil 233 } 234 235 // sync flushes all data tables to disk. 236 func (f *freezer) Sync() error { 237 var errs []error 238 for _, table := range f.tables { 239 if err := table.Sync(); err != nil { 240 errs = append(errs, err) 241 } 242 } 243 if errs != nil { 244 return fmt.Errorf("%v", errs) 245 } 246 return nil 247 } 248 249 // freeze is a background thread that periodically checks the blockchain for any 250 // import progress and moves ancient data from the fast database into the freezer. 251 // 252 // This functionality is deliberately broken off from block importing to avoid 253 // incurring additional data shuffling delays on block propagation. 254 func (f *freezer) freeze(db ethdb.KeyValueStore) { 255 nfdb := &nofreezedb{KeyValueStore: db} 256 257 for { 258 // Retrieve the freezing threshold. 259 hash := ReadHeadBlockHash(nfdb) 260 if hash == (common.Hash{}) { 261 log.Debug("Current full block hash unavailable") // new chain, empty database 262 time.Sleep(freezerRecheckInterval) 263 continue 264 } 265 number := ReadHeaderNumber(nfdb, hash) 266 switch { 267 case number == nil: 268 log.Error("Current full block number unavailable", "hash", hash) 269 time.Sleep(freezerRecheckInterval) 270 continue 271 272 case *number < params.ImmutabilityThreshold: 273 time.Sleep(freezerRecheckInterval) 274 continue 275 276 case *number-params.ImmutabilityThreshold <= f.frozen: 277 log.Debug("Ancient blocks frozen already", "number", *number, "hash", hash, "frozen", f.frozen) 278 time.Sleep(freezerRecheckInterval) 279 continue 280 } 281 head := ReadHeader(nfdb, hash, *number) 282 if head == nil { 283 log.Error("Current full block unavailable", "number", *number, "hash", hash) 284 time.Sleep(freezerRecheckInterval) 285 continue 286 } 287 // Seems we have data ready to be frozen, process in usable batches 288 limit := *number - params.ImmutabilityThreshold 289 if limit-f.frozen > freezerBatchLimit { 290 limit = f.frozen + freezerBatchLimit 291 } 292 var ( 293 start = time.Now() 294 first = f.frozen 295 ancients = make([]common.Hash, 0, limit) 296 ) 297 for f.frozen < limit { 298 // Retrieves all the components of the canonical block 299 hash := ReadCanonicalHash(nfdb, f.frozen) 300 if hash == (common.Hash{}) { 301 log.Error("Canonical hash missing, can't freeze", "number", f.frozen) 302 break 303 } 304 header := ReadHeaderRLP(nfdb, hash, f.frozen) 305 if len(header) == 0 { 306 log.Error("Block header missing, can't freeze", "number", f.frozen, "hash", hash) 307 break 308 } 309 body := ReadBodyRLP(nfdb, hash, f.frozen) 310 if len(body) == 0 { 311 log.Error("Block body missing, can't freeze", "number", f.frozen, "hash", hash) 312 break 313 } 314 receipts := ReadReceiptsRLP(nfdb, hash, f.frozen) 315 if len(receipts) == 0 { 316 log.Error("Block receipts missing, can't freeze", "number", f.frozen, "hash", hash) 317 break 318 } 319 td := ReadTdRLP(nfdb, hash, f.frozen) 320 if len(td) == 0 { 321 log.Error("Total difficulty missing, can't freeze", "number", f.frozen, "hash", hash) 322 break 323 } 324 log.Trace("Deep froze ancient block", "number", f.frozen, "hash", hash) 325 // Inject all the components into the relevant data tables 326 if err := f.AppendAncient(f.frozen, hash[:], header, body, receipts, td); err != nil { 327 break 328 } 329 ancients = append(ancients, hash) 330 } 331 // Batch of blocks have been frozen, flush them before wiping from leveldb 332 if err := f.Sync(); err != nil { 333 log.Crit("Failed to flush frozen tables", "err", err) 334 } 335 // Wipe out all data from the active database 336 batch := db.NewBatch() 337 for i := 0; i < len(ancients); i++ { 338 // Always keep the genesis block in active database 339 if first+uint64(i) != 0 { 340 DeleteBlockWithoutNumber(batch, ancients[i], first+uint64(i)) 341 DeleteCanonicalHash(batch, first+uint64(i)) 342 } 343 } 344 if err := batch.Write(); err != nil { 345 log.Crit("Failed to delete frozen canonical blocks", "err", err) 346 } 347 batch.Reset() 348 // Wipe out side chain also. 349 for number := first; number < f.frozen; number++ { 350 // Always keep the genesis block in active database 351 if number != 0 { 352 for _, hash := range ReadAllHashes(db, number) { 353 DeleteBlock(batch, hash, number) 354 } 355 } 356 } 357 if err := batch.Write(); err != nil { 358 log.Crit("Failed to delete frozen side blocks", "err", err) 359 } 360 // Log something friendly for the user 361 context := []interface{}{ 362 "blocks", f.frozen - first, "elapsed", common.PrettyDuration(time.Since(start)), "number", f.frozen - 1, 363 } 364 if n := len(ancients); n > 0 { 365 context = append(context, []interface{}{"hash", ancients[n-1]}...) 366 } 367 log.Info("Deep froze chain segment", context...) 368 369 // Avoid database thrashing with tiny writes 370 if f.frozen-first < freezerBatchLimit { 371 time.Sleep(freezerRecheckInterval) 372 } 373 } 374 } 375 376 // repair truncates all data tables to the same length. 377 func (f *freezer) repair() error { 378 min := uint64(math.MaxUint64) 379 for _, table := range f.tables { 380 items := atomic.LoadUint64(&table.items) 381 if min > items { 382 min = items 383 } 384 } 385 for _, table := range f.tables { 386 if err := table.truncate(min); err != nil { 387 return err 388 } 389 } 390 atomic.StoreUint64(&f.frozen, min) 391 return nil 392 }