github.com/MikyChow/arbitrum-go-ethereum@v0.0.0-20230306102812-078da49636de/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/MikyChow/arbitrum-go-ethereum/common" 30 "github.com/MikyChow/arbitrum-go-ethereum/ethdb" 31 "github.com/MikyChow/arbitrum-go-ethereum/log" 32 "github.com/MikyChow/arbitrum-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 Releaser // File-system lock to prevent double opens 78 closeOnce sync.Once 79 } 80 81 // NewFreezer creates a freezer instance for maintaining immutable ordered 82 // data according to the given parameters. 83 // 84 // The 'tables' argument defines the data tables. If the value of a map 85 // entry is true, snappy compression is disabled for the table. 86 func NewFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*Freezer, error) { 87 // Create the initial freezer object 88 var ( 89 readMeter = metrics.NewRegisteredMeter(namespace+"ancient/read", nil) 90 writeMeter = metrics.NewRegisteredMeter(namespace+"ancient/write", nil) 91 sizeGauge = metrics.NewRegisteredGauge(namespace+"ancient/size", nil) 92 ) 93 // Ensure the datadir is not a symbolic link if it exists. 94 if info, err := os.Lstat(datadir); !os.IsNotExist(err) { 95 if info.Mode()&os.ModeSymlink != 0 { 96 log.Warn("Symbolic link ancient database is not supported", "path", datadir) 97 return nil, errSymlinkDatadir 98 } 99 } 100 // Leveldb uses LOCK as the filelock filename. To prevent the 101 // name collision, we use FLOCK as the lock name. 102 lock, _, err := Flock(filepath.Join(datadir, "FLOCK")) 103 if err != nil { 104 return nil, err 105 } 106 // Open all the supported data tables 107 freezer := &Freezer{ 108 readonly: readonly, 109 tables: make(map[string]*freezerTable), 110 instanceLock: lock, 111 } 112 113 // Create the tables. 114 for name, disableSnappy := range tables { 115 table, err := newTable(datadir, name, readMeter, writeMeter, sizeGauge, maxTableSize, disableSnappy, readonly) 116 if err != nil { 117 for _, table := range freezer.tables { 118 table.Close() 119 } 120 lock.Release() 121 return nil, err 122 } 123 freezer.tables[name] = table 124 } 125 126 if freezer.readonly { 127 // In readonly mode only validate, don't truncate. 128 // validate also sets `freezer.frozen`. 129 err = freezer.validate() 130 } else { 131 // Truncate all tables to common length. 132 err = freezer.repair() 133 } 134 if err != nil { 135 for _, table := range freezer.tables { 136 table.Close() 137 } 138 lock.Release() 139 return nil, err 140 } 141 142 // Create the write batch. 143 freezer.writeBatch = newFreezerBatch(freezer) 144 145 log.Info("Opened ancient database", "database", datadir, "readonly", readonly) 146 return freezer, nil 147 } 148 149 // Close terminates the chain freezer, unmapping all the data files. 150 func (f *Freezer) Close() error { 151 f.writeLock.Lock() 152 defer f.writeLock.Unlock() 153 154 var errs []error 155 f.closeOnce.Do(func() { 156 for _, table := range f.tables { 157 if err := table.Close(); err != nil { 158 errs = append(errs, err) 159 } 160 } 161 if err := f.instanceLock.Release(); err != nil { 162 errs = append(errs, err) 163 } 164 }) 165 if errs != nil { 166 return fmt.Errorf("%v", errs) 167 } 168 return nil 169 } 170 171 // HasAncient returns an indicator whether the specified ancient data exists 172 // in the freezer. 173 func (f *Freezer) HasAncient(kind string, number uint64) (bool, error) { 174 if table := f.tables[kind]; table != nil { 175 return table.has(number), nil 176 } 177 return false, nil 178 } 179 180 // Ancient retrieves an ancient binary blob from the append-only immutable files. 181 func (f *Freezer) Ancient(kind string, number uint64) ([]byte, error) { 182 if table := f.tables[kind]; table != nil { 183 return table.Retrieve(number) 184 } 185 return nil, errUnknownTable 186 } 187 188 // AncientRange retrieves multiple items in sequence, starting from the index 'start'. 189 // It will return 190 // - at most 'max' items, 191 // - at least 1 item (even if exceeding the maxByteSize), but will otherwise 192 // return as many items as fit into maxByteSize. 193 func (f *Freezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) { 194 if table := f.tables[kind]; table != nil { 195 return table.RetrieveItems(start, count, maxBytes) 196 } 197 return nil, errUnknownTable 198 } 199 200 // Ancients returns the length of the frozen items. 201 func (f *Freezer) Ancients() (uint64, error) { 202 return atomic.LoadUint64(&f.frozen), nil 203 } 204 205 // Tail returns the number of first stored item in the freezer. 206 func (f *Freezer) Tail() (uint64, error) { 207 return atomic.LoadUint64(&f.tail), nil 208 } 209 210 // AncientSize returns the ancient size of the specified category. 211 func (f *Freezer) AncientSize(kind string) (uint64, error) { 212 // This needs the write lock to avoid data races on table fields. 213 // Speed doesn't matter here, AncientSize is for debugging. 214 f.writeLock.RLock() 215 defer f.writeLock.RUnlock() 216 217 if table := f.tables[kind]; table != nil { 218 return table.size() 219 } 220 return 0, errUnknownTable 221 } 222 223 // ReadAncients runs the given read operation while ensuring that no writes take place 224 // on the underlying freezer. 225 func (f *Freezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) { 226 f.writeLock.RLock() 227 defer f.writeLock.RUnlock() 228 229 return fn(f) 230 } 231 232 // ModifyAncients runs the given write operation. 233 func (f *Freezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) { 234 if f.readonly { 235 return 0, errReadOnly 236 } 237 f.writeLock.Lock() 238 defer f.writeLock.Unlock() 239 240 // Roll back all tables to the starting position in case of error. 241 prevItem := atomic.LoadUint64(&f.frozen) 242 defer func() { 243 if err != nil { 244 // The write operation has failed. Go back to the previous item position. 245 for name, table := range f.tables { 246 err := table.truncateHead(prevItem) 247 if err != nil { 248 log.Error("Freezer table roll-back failed", "table", name, "index", prevItem, "err", err) 249 } 250 } 251 } 252 }() 253 254 f.writeBatch.reset() 255 if err := fn(f.writeBatch); err != nil { 256 return 0, err 257 } 258 item, writeSize, err := f.writeBatch.commit() 259 if err != nil { 260 return 0, err 261 } 262 atomic.StoreUint64(&f.frozen, item) 263 return writeSize, nil 264 } 265 266 // TruncateHead discards any recent data above the provided threshold number. 267 func (f *Freezer) TruncateHead(items uint64) error { 268 if f.readonly { 269 return errReadOnly 270 } 271 f.writeLock.Lock() 272 defer f.writeLock.Unlock() 273 274 if atomic.LoadUint64(&f.frozen) <= items { 275 return nil 276 } 277 for _, table := range f.tables { 278 if err := table.truncateHead(items); err != nil { 279 return err 280 } 281 } 282 atomic.StoreUint64(&f.frozen, items) 283 return nil 284 } 285 286 // TruncateTail discards any recent data below the provided threshold number. 287 func (f *Freezer) TruncateTail(tail uint64) error { 288 if f.readonly { 289 return errReadOnly 290 } 291 f.writeLock.Lock() 292 defer f.writeLock.Unlock() 293 294 if atomic.LoadUint64(&f.tail) >= tail { 295 return nil 296 } 297 for _, table := range f.tables { 298 if err := table.truncateTail(tail); err != nil { 299 return err 300 } 301 } 302 atomic.StoreUint64(&f.tail, tail) 303 return nil 304 } 305 306 // Sync flushes all data tables to disk. 307 func (f *Freezer) Sync() error { 308 var errs []error 309 for _, table := range f.tables { 310 if err := table.Sync(); err != nil { 311 errs = append(errs, err) 312 } 313 } 314 if errs != nil { 315 return fmt.Errorf("%v", errs) 316 } 317 return nil 318 } 319 320 // validate checks that every table has the same length. 321 // Used instead of `repair` in readonly mode. 322 func (f *Freezer) validate() error { 323 if len(f.tables) == 0 { 324 return nil 325 } 326 var ( 327 length uint64 328 name string 329 ) 330 // Hack to get length of any table 331 for kind, table := range f.tables { 332 length = atomic.LoadUint64(&table.items) 333 name = kind 334 break 335 } 336 // Now check every table against that length 337 for kind, table := range f.tables { 338 items := atomic.LoadUint64(&table.items) 339 if length != items { 340 return fmt.Errorf("freezer tables %s and %s have differing lengths: %d != %d", kind, name, items, length) 341 } 342 } 343 atomic.StoreUint64(&f.frozen, length) 344 return nil 345 } 346 347 // repair truncates all data tables to the same length. 348 func (f *Freezer) repair() error { 349 var ( 350 head = uint64(math.MaxUint64) 351 tail = uint64(0) 352 ) 353 for _, table := range f.tables { 354 items := atomic.LoadUint64(&table.items) 355 if head > items { 356 head = items 357 } 358 hidden := atomic.LoadUint64(&table.itemHidden) 359 if hidden > tail { 360 tail = hidden 361 } 362 } 363 for _, table := range f.tables { 364 if err := table.truncateHead(head); err != nil { 365 return err 366 } 367 if err := table.truncateTail(tail); err != nil { 368 return err 369 } 370 } 371 atomic.StoreUint64(&f.frozen, head) 372 atomic.StoreUint64(&f.tail, tail) 373 return nil 374 } 375 376 // convertLegacyFn takes a raw freezer entry in an older format and 377 // returns it in the new format. 378 type convertLegacyFn = func([]byte) ([]byte, error) 379 380 // MigrateTable processes the entries in a given table in sequence 381 // converting them to a new format if they're of an old format. 382 func (f *Freezer) MigrateTable(kind string, convert convertLegacyFn) error { 383 if f.readonly { 384 return errReadOnly 385 } 386 f.writeLock.Lock() 387 defer f.writeLock.Unlock() 388 389 table, ok := f.tables[kind] 390 if !ok { 391 return errUnknownTable 392 } 393 // forEach iterates every entry in the table serially and in order, calling `fn` 394 // with the item as argument. If `fn` returns an error the iteration stops 395 // and that error will be returned. 396 forEach := func(t *freezerTable, offset uint64, fn func(uint64, []byte) error) error { 397 var ( 398 items = atomic.LoadUint64(&t.items) 399 batchSize = uint64(1024) 400 maxBytes = uint64(1024 * 1024) 401 ) 402 for i := offset; i < items; { 403 if i+batchSize > items { 404 batchSize = items - i 405 } 406 data, err := t.RetrieveItems(i, batchSize, maxBytes) 407 if err != nil { 408 return err 409 } 410 for j, item := range data { 411 if err := fn(i+uint64(j), item); err != nil { 412 return err 413 } 414 } 415 i += uint64(len(data)) 416 } 417 return nil 418 } 419 // TODO(s1na): This is a sanity-check since as of now no process does tail-deletion. But the migration 420 // process assumes no deletion at tail and needs to be modified to account for that. 421 if table.itemOffset > 0 || table.itemHidden > 0 { 422 return fmt.Errorf("migration not supported for tail-deleted freezers") 423 } 424 ancientsPath := filepath.Dir(table.index.Name()) 425 // Set up new dir for the migrated table, the content of which 426 // we'll at the end move over to the ancients dir. 427 migrationPath := filepath.Join(ancientsPath, "migration") 428 newTable, err := newFreezerTable(migrationPath, kind, table.noCompression, false) 429 if err != nil { 430 return err 431 } 432 var ( 433 batch = newTable.newBatch() 434 out []byte 435 start = time.Now() 436 logged = time.Now() 437 offset = newTable.items 438 ) 439 if offset > 0 { 440 log.Info("found previous migration attempt", "migrated", offset) 441 } 442 // Iterate through entries and transform them 443 if err := forEach(table, offset, func(i uint64, blob []byte) error { 444 if i%10000 == 0 && time.Since(logged) > 16*time.Second { 445 log.Info("Processing legacy elements", "count", i, "elapsed", common.PrettyDuration(time.Since(start))) 446 logged = time.Now() 447 } 448 out, err = convert(blob) 449 if err != nil { 450 return err 451 } 452 if err := batch.AppendRaw(i, out); err != nil { 453 return err 454 } 455 return nil 456 }); err != nil { 457 return err 458 } 459 if err := batch.commit(); err != nil { 460 return err 461 } 462 log.Info("Replacing old table files with migrated ones", "elapsed", common.PrettyDuration(time.Since(start))) 463 // Release and delete old table files. Note this won't 464 // delete the index file. 465 table.releaseFilesAfter(0, true) 466 467 if err := newTable.Close(); err != nil { 468 return err 469 } 470 files, err := os.ReadDir(migrationPath) 471 if err != nil { 472 return err 473 } 474 // Move migrated files to ancients dir. 475 for _, f := range files { 476 // This will replace the old index file as a side-effect. 477 if err := os.Rename(filepath.Join(migrationPath, f.Name()), filepath.Join(ancientsPath, f.Name())); err != nil { 478 return err 479 } 480 } 481 // Delete by now empty dir. 482 if err := os.Remove(migrationPath); err != nil { 483 return err 484 } 485 return nil 486 }