github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/wal/wal.go (about) 1 package wal 2 3 import ( 4 "bufio" 5 "encoding/base64" 6 "encoding/binary" 7 goerrors "errors" 8 "fmt" 9 "hash/crc32" 10 "io" 11 "log/slog" 12 "path/filepath" 13 "time" 14 15 "github.com/gnolang/gno/tm2/pkg/amino" 16 auto "github.com/gnolang/gno/tm2/pkg/autofile" 17 tmtime "github.com/gnolang/gno/tm2/pkg/bft/types/time" 18 "github.com/gnolang/gno/tm2/pkg/errors" 19 osm "github.com/gnolang/gno/tm2/pkg/os" 20 "github.com/gnolang/gno/tm2/pkg/service" 21 ) 22 23 const ( 24 // how often the WAL should be sync'd during period sync'ing 25 walDefaultFlushInterval = 2 * time.Second 26 ) 27 28 var ( 29 crc32c = crc32.MakeTable(crc32.Castagnoli) 30 base64stdnp = base64.StdEncoding.WithPadding(base64.NoPadding) 31 ) 32 33 // -------------------------------------------------------- 34 // types and functions for savings consensus messages 35 36 type WALMessage interface { 37 AssertWALMessage() 38 } 39 40 // TimedWALMessage wraps WALMessage and adds Time for debugging purposes. 41 type TimedWALMessage struct { 42 Time time.Time `json:"time"` 43 Msg WALMessage `json:"msg"` 44 } 45 46 // Some lines are MetaMessages to denote new heights, etc. 47 // NOTE: The encoding must not contain the '#' character, 48 // so arbitrary strings will not work without some kind of escaping. 49 // TODO: consider alternative meta line schemas in the long run, or escape the 50 // '#' character in such a way that scanning randomly from any position in a 51 // file resumes correctly after the first and only likely corruption. 52 type MetaMessage struct { 53 Height int64 `json:"h"` 54 } 55 56 // -------------------------------------------------------- 57 // Simple write-ahead logger 58 59 // WAL is an interface for any write-ahead logger. 60 type WAL interface { 61 // config methods 62 SetLogger(l *slog.Logger) 63 64 // write methods 65 Write(WALMessage) error 66 WriteSync(WALMessage) error 67 WriteMetaSync(MetaMessage) error 68 FlushAndSync() error 69 70 // search methods 71 SearchForHeight(height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error) 72 73 // service methods 74 Start() error 75 Stop() error 76 Wait() 77 } 78 79 // Write ahead logger writes msgs to disk before they are processed. 80 // Can be used for crash-recovery and deterministic replay. 81 // TODO: currently the wal is overwritten during replay catchup, give it a mode 82 // so it's either reading or appending - must read to end to start appending 83 // again. 84 type baseWAL struct { 85 service.BaseService 86 87 group *auto.Group 88 89 maxSize int64 90 enc *WALWriter 91 92 flushTicker *time.Ticker 93 flushInterval time.Duration 94 } 95 96 var _ WAL = &baseWAL{} 97 98 // NewWAL returns a new write-ahead logger based on `baseWAL`, which implements 99 // WAL. It's flushed and synced to disk every 2s and once when stopped. 100 // `maxSize` is the maximum allowable amino bytes of a TimedWALMessage 101 // including the amino (byte) size prefix, but excluding any crc checks. 102 func NewWAL(walFile string, maxSize int64, groupOptions ...func(*auto.Group)) (*baseWAL, error) { 103 err := osm.EnsureDir(filepath.Dir(walFile), 0o700) 104 if err != nil { 105 return nil, errors.Wrap(err, "failed to ensure WAL directory is in place") 106 } 107 108 group, err := auto.OpenGroup(walFile, groupOptions...) 109 if err != nil { 110 return nil, err 111 } 112 wal := &baseWAL{ 113 group: group, 114 maxSize: maxSize, 115 enc: NewWALWriter(group, maxSize), 116 flushInterval: walDefaultFlushInterval, 117 } 118 wal.BaseService = *service.NewBaseService(nil, "baseWAL", wal) 119 return wal, nil 120 } 121 122 // SetFlushInterval allows us to override the periodic flush interval for the WAL. 123 func (wal *baseWAL) SetFlushInterval(i time.Duration) { 124 wal.flushInterval = i 125 } 126 127 func (wal *baseWAL) Group() *auto.Group { 128 return wal.group 129 } 130 131 func (wal *baseWAL) SetLogger(l *slog.Logger) { 132 wal.BaseService.Logger = l 133 wal.group.SetLogger(l) 134 } 135 136 func (wal *baseWAL) OnStart() error { 137 size := wal.group.ReadGroupInfo().TotalSize 138 if size == 0 { 139 wal.WriteMetaSync(MetaMessage{Height: 0}) 140 } 141 err := wal.group.Start() 142 if err != nil { 143 return err 144 } 145 wal.flushTicker = time.NewTicker(wal.flushInterval) 146 go wal.processFlushTicks() 147 return nil 148 } 149 150 func (wal *baseWAL) processFlushTicks() { 151 for { 152 select { 153 case <-wal.flushTicker.C: 154 if err := wal.FlushAndSync(); err != nil { 155 wal.Logger.Error("Periodic WAL flush failed", "err", err) 156 } 157 case <-wal.Quit(): 158 return 159 } 160 } 161 } 162 163 // FlushAndSync flushes and fsync's the underlying group's data to disk. 164 // See auto#FlushAndSync 165 func (wal *baseWAL) FlushAndSync() error { 166 return wal.group.FlushAndSync() 167 } 168 169 // Stop the underlying autofile group. 170 // Use Wait() to ensure it's finished shutting down 171 // before cleaning up files. 172 func (wal *baseWAL) OnStop() { 173 wal.flushTicker.Stop() 174 wal.FlushAndSync() 175 wal.group.Stop() 176 wal.group.Close() 177 } 178 179 // Wait for the underlying autofile group to finish shutting down 180 // so it's safe to cleanup files. 181 func (wal *baseWAL) Wait() { 182 wal.group.Wait() 183 } 184 185 // Write is called in newStep and for each receive on the 186 // peerMsgQueue and the timeoutTicker. 187 // NOTE: does not call fsync() 188 func (wal *baseWAL) Write(msg WALMessage) error { 189 if wal == nil { 190 return nil 191 } 192 193 if err := wal.enc.Write(TimedWALMessage{tmtime.Now(), msg}); err != nil { 194 wal.Logger.Error("Error writing msg to consensus wal. WARNING: recover may not be possible for the current height", 195 "err", err, "msg", msg) 196 return err 197 } 198 199 return nil 200 } 201 202 // WriteMetaSync writes the new height and finalizes the previous height. 203 // NOTE: It is useless to implement WriteMeta() (asynchronous) because there is 204 // usually something to do in sync after the aforementioned finalization 205 // occurs. 206 func (wal *baseWAL) WriteMetaSync(meta MetaMessage) error { 207 if wal == nil { 208 return nil 209 } 210 211 if err := wal.enc.WriteMeta(meta); err != nil { 212 wal.Logger.Error("Error writing height to consensus wal. WARNING: full recover may not be possible for the previous height", 213 "err", err) 214 return err 215 } 216 217 if err := wal.FlushAndSync(); err != nil { 218 wal.Logger.Error("WriteSync failed to flush consensus wal. WARNING: may result in creating alternative proposals / votes for the current height iff the node restarted", 219 "err", err) 220 return err 221 } 222 223 return nil 224 } 225 226 // WriteSync is called when we receive a msg from ourselves 227 // so that we write to disk before sending signed messages. 228 // NOTE: calls fsync() 229 func (wal *baseWAL) WriteSync(msg WALMessage) error { 230 if wal == nil { 231 return nil 232 } 233 234 if err := wal.Write(msg); err != nil { 235 return err 236 } 237 238 if err := wal.FlushAndSync(); err != nil { 239 wal.Logger.Error("WriteSync failed to flush consensus wal. WARNING: may result in creating alternative proposals / votes for the current height iff the node restarted", 240 "err", err) 241 return err 242 } 243 244 return nil 245 } 246 247 type WALSearchMode int 248 249 const ( 250 WALSearchModeInvalid WALSearchMode = iota 251 WALSearchModeBackwards 252 WALSearchModeBinary 253 ) 254 255 // WALSearchOptions are optional arguments to SearchForHeight. 256 type WALSearchOptions struct { 257 // Mode is by default backwards via WALSearchModeBackwards (limit at 100 258 // blocks). 259 Mode WALSearchMode 260 // IgnoreDataCorruptionErrors set to true will result in skipping data 261 // corruption errors (default false). 262 IgnoreDataCorruptionErrors bool 263 } 264 265 // SearchForHeight scans meta lines to find the first line after height as 266 // denoted by a meta line, and returns an auto.GroupReader. Group reader will 267 // be nil if found equals false. 268 // 269 // The result is a buffered ReadCloser. 270 // 271 // CONTRACT: caller must close group reader. 272 func (wal *baseWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error) { 273 var ( 274 msg *TimedWALMessage 275 meta *MetaMessage 276 gr *auto.GroupReader 277 ) 278 279 // NOTE: starting from the last file in the group because we're usually 280 // searching for the last height. See replay.go 281 min, max := wal.group.MinIndex(), wal.group.MaxIndex() 282 wal.Logger.Info("Searching for height", "height", height, "min", min, "max", max) 283 284 var ( 285 mode = WALSearchModeBackwards 286 backoff = 0 // backwards search offset (goes 0,-1,-2,-4,-8,-16...) 287 idxoff = 0 // forward index offset (for when file contains no meta lines). 288 ) 289 290 // option overrides. 291 if options != nil && options.Mode != WALSearchModeInvalid { 292 mode = options.Mode 293 } 294 295 OUTER_LOOP: 296 for min <= max { 297 var index int 298 299 // set index depending on mode. 300 switch mode { 301 case WALSearchModeBackwards: 302 index = max + backoff + idxoff 303 if max < index { 304 // (max+backoff)+ doesn't contain any height. 305 // adjust max & backoff accordingly. 306 idxoff = 0 307 max = max + backoff - 1 308 if backoff == 0 { 309 backoff = -1 310 } else { 311 backoff = backoff * 2 // exponential backwards 312 } 313 continue OUTER_LOOP 314 } 315 if index < min { 316 panic("should not happen") 317 } 318 case WALSearchModeBinary: 319 index = (min+max+1)/2 + idxoff 320 if max < index { 321 // ((min+max+1)/2)+ doesn't contain any height. 322 // adjust max & binary search accordingly. 323 idxoff = 0 324 max = (min+max+1)/2 - 1 325 continue OUTER_LOOP 326 } 327 } 328 329 gr, err = wal.group.NewReader(index, index+1) // read only for index. 330 if err != nil { 331 return nil, false, err 332 } 333 dec := NewWALReader(gr, wal.maxSize) 334 335 FILE_LOOP: 336 for { 337 msg, meta, err = dec.ReadMessage() 338 // error case 339 if err != nil { 340 if goerrors.Is(err, io.EOF) { 341 // adjust next index. 342 // @index didn't have a height declaration. 343 idxoff++ 344 dec.Close() 345 continue OUTER_LOOP 346 } else if options != nil && options.IgnoreDataCorruptionErrors && IsDataCorruptionError(err) { 347 wal.Logger.Error("Corrupted entry. Skipping...", "err", err) 348 continue FILE_LOOP // skip corrupted line and ignore error. 349 } else { 350 dec.Close() 351 return nil, false, err 352 } 353 } 354 // meta case 355 if meta != nil { 356 if height < meta.Height { 357 // adjust next index. 358 // it's in an earlier position or file. 359 switch mode { 360 case WALSearchModeBackwards: 361 idxoff = 0 362 if backoff == 0 { 363 max-- 364 backoff = -1 365 } else { 366 max += backoff 367 backoff *= 2 368 } 369 // convert to binary search if backoff is too big. 370 // max+backoff would work but max+(backoff*2) is smoother. 371 if max+(backoff*2) <= min { 372 wal.Logger.Info("Converting to binary search", 373 "height", height, "min", min, 374 "max", max, "backoff", backoff) 375 backoff = 0 376 mode = WALSearchModeBinary 377 } 378 case WALSearchModeBinary: 379 idxoff = 0 380 max = (min+max+1)/2 - 1 381 } 382 dec.Close() 383 continue OUTER_LOOP 384 } else if meta.Height == height { // found 385 wal.Logger.Info("Found", "height", height, "index", index) 386 // NOTE: dec itself is an io.ReadCloser for this purpose. 387 // NOTE: the result is buffered, specifically a bufio.Reader. 388 return dec, true, nil 389 } else { // meta.Height < height 390 // it comes later, maybe @index. 391 switch mode { 392 case WALSearchModeBackwards: 393 if backoff == 0 { 394 // ignore and keep reading 395 // NOTE: in the future we could binary search 396 // within a file, but for now we read sequentially. 397 continue FILE_LOOP 398 } else { 399 // convert to binary search with index as new min. 400 wal.Logger.Info("Converting to binary search with new min", 401 "height", height, "min", min, 402 "max", max, "backoff", backoff) 403 idxoff = 0 404 backoff = 0 405 min = index 406 mode = WALSearchModeBinary 407 dec.Close() 408 continue OUTER_LOOP 409 } 410 case WALSearchModeBinary: 411 if index < max { 412 // maybe in @index, but first try binary search 413 // between @index and max. 414 idxoff = 0 415 min = index 416 dec.Close() 417 continue OUTER_LOOP 418 } else { // index == max 419 // this is the last file, keep reading. 420 // NOTE: in the future we could binary search 421 // within a file, but for now we read sequentially. 422 continue FILE_LOOP 423 } 424 } 425 } 426 } 427 // msg case 428 if msg != nil { 429 // do nothing, we're only interested in meta lines. 430 // TODO: optimize by implementing ReadNextMeta(), 431 // which skips decoding non-meta messages. 432 } 433 } 434 } 435 436 return nil, false, nil 437 } 438 439 // ----------- 440 441 // A WALWriter writes custom-encoded WAL messages to an output stream. 442 // Each binary WAL entry is length encoded, then crc encoded, 443 // then base64 encoded and delimited by newlines. 444 // The base64 encoding used is encodeStd, 445 // `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/`. 446 // 447 // Each WAL item is also newline delimited. 448 // 449 // Meta lines are prefixed with a '#' (which is not a valid base64 character) 450 // denote meta information, such as new height. The purpose of base64 451 // encoding is to enable backwards traversal of items (e.g. in search of some 452 // previous height). 453 // 454 // Base64 is chosen to optimize the worst-case scenario for encoding delimited 455 // binary bytes while enabling metadata insertion and also supporting 456 // backwards traversal of binary blobs (to enable binary search, etc). In the 457 // future, base64 could be replaced with a similar encoding formula, and the 458 // crc function could change too, but otherwise the structure of the WAL 459 // should not change, including the rule that all metalines should start with 460 // '#' and that all items be delimited with a newline. This implementation 461 // enforces ASCII text characters, but other implementations may choose 462 // otherwise. 463 // 464 // Format: base64(4 bytes CRC sum + length-prefixed amino bytes) + newline. 465 // e.g. 466 // ``` 467 // ABCDEFGHIJKLMNOPQRSTUV00 468 // ABCDEFGHIJKLMNOPQRSTUV01 469 // ABCDEFGHIJKLMNOPQRSTUV02 470 // #{"h":"123"} 471 // ABCDEFGHIJKLMNOPQRSTUV03 472 // ABCDEFGHIJKLMNOPQRSTUV04 473 type WALWriter struct { 474 wr io.Writer 475 maxSize int64 // max WALMessage amino size excluding time/crc/base64. 476 } 477 478 // NewWALWriter returns a new encoder that writes to wr. 479 func NewWALWriter(wr io.Writer, maxSize int64) *WALWriter { 480 return &WALWriter{wr, maxSize} 481 } 482 483 // Write writes the custom encoding of v to the stream, followed by a newline 484 // byte. It returns an error if the amino-encoded size of v is greater than 485 // maxSize. Any error encountered during the write is also returned. 486 func (enc *WALWriter) Write(v TimedWALMessage) error { 487 twmBytes := amino.MustMarshalSized(v) 488 489 length := int64(len(twmBytes)) 490 if 0 < enc.maxSize && enc.maxSize < length { 491 return fmt.Errorf("msg is too big: %d bytes, max: %d bytes", length, enc.maxSize) 492 } 493 494 totalLength := 4 + int(length) 495 crc := crc32.Checksum(twmBytes, crc32c) 496 line := make([]byte, totalLength) 497 binary.BigEndian.PutUint32(line[0:4], crc) 498 copy(line[4:], twmBytes) 499 500 line64 := base64stdnp.EncodeToString(line) 501 line64 += "\n" 502 _, err := enc.wr.Write([]byte(line64)) 503 return err 504 } 505 506 // Meta lines are in JSON for readability. 507 // TODO: CRC not used (yet), concatenate the CRC bytes with the JSON bytes. 508 func (enc *WALWriter) WriteMeta(meta MetaMessage) error { 509 metaJSON := amino.MustMarshalJSON(meta) 510 metaLine := "#" + string(metaJSON) + "\n" 511 _, err := enc.wr.Write([]byte(metaLine)) 512 return err 513 } 514 515 // ----------- 516 517 // IsDataCorruptionError returns true if data has been corrupted inside WAL. 518 func IsDataCorruptionError(err error) bool { 519 _, ok := err.(DataCorruptionError) 520 return ok 521 } 522 523 // DataCorruptionError is an error that occurs if data on disk was corrupted. 524 type DataCorruptionError struct { 525 cause error 526 } 527 528 func (e DataCorruptionError) Error() string { 529 return fmt.Sprintf("DataCorruptionError[%v]", e.cause) 530 } 531 532 func (e DataCorruptionError) Cause() error { 533 return e.cause 534 } 535 536 // A WALReader reads and decodes custom-encoded WAL messages from an input 537 // stream. See WALWriter for the format used. 538 // 539 // It will also compare the checksums and make sure data size is equal to the 540 // length from the header. If that is not the case, error will be returned. 541 // 542 // WALReader is itself an io.ReaderCloser, and it uses a bufio reader under the 543 // hood, which means it will usually end up reading more bytes than was 544 // actually returned via calls to ReadMessage(). 545 type WALReader struct { 546 rd io.Reader // NOTE: use brd instead. 547 brd *bufio.Reader 548 maxSize int64 549 } 550 551 // NewWALReader returns a new decoder that reads from rd. 552 func NewWALReader(rd io.Reader, maxSize int64) *WALReader { 553 return &WALReader{rd, bufio.NewReader(rd), maxSize} 554 } 555 556 // Reads a line until the "\n" delimiter byte, and returns that line without 557 // the delimiter. 558 // A line must end with "\n", otherwise EOF. 559 func (dec *WALReader) readline() ([]byte, error) { 560 bz, err := dec.brd.ReadBytes('\n') 561 if 0 < len(bz) { 562 bz = bz[:len(bz)-1] 563 } 564 return bz, err 565 } 566 567 // Implement io.ReadCloser for SearchForHeight() result reader. 568 func (dec *WALReader) Read(p []byte) (n int, err error) { 569 return dec.brd.Read(p) 570 } 571 572 // Implement io.ReadCloser for SearchForHeight() result reader. 573 func (dec *WALReader) Close() (err error) { 574 // There is no corresponding .Close on a bufio. 575 // we will set brd to nil and let the program panic 576 // when methods are called after a close. 577 dec.brd = nil 578 579 // Close rd if it is a Closer. 580 if cl, ok := dec.rd.(io.Closer); ok { 581 err = cl.Close() 582 return 583 } 584 585 return 586 } 587 588 // Decode reads the next custom-encoded value from its reader and returns it. 589 // One TimedWALMessage or MetaError or error is returned, the rest are nil. 590 func (dec *WALReader) ReadMessage() (*TimedWALMessage, *MetaMessage, error) { 591 line64, err := dec.readline() 592 if err != nil { 593 return nil, nil, err 594 } 595 596 if len(line64) == 0 { 597 return nil, nil, DataCorruptionError{fmt.Errorf("found empty line")} 598 } 599 600 // special case for MetaMessage. 601 if line64[0] == '#' { 602 var meta MetaMessage 603 err := amino.UnmarshalJSON(line64[1:], &meta) 604 return nil, &meta, err 605 } 606 607 // is usual TimedWALMessage. 608 // decode base64. 609 line, err := base64stdnp.DecodeString(string(line64)) 610 if err != nil { 611 return nil, nil, DataCorruptionError{fmt.Errorf("failed to decode base64: %w", err)} 612 } 613 614 // read crc out of bytes. 615 crcSize := int64(4) 616 if int64(len(line)) < crcSize { 617 return nil, nil, DataCorruptionError{fmt.Errorf("failed to read checksum: %w", err)} 618 } 619 crc, twmBytes := binary.BigEndian.Uint32(line[:crcSize]), line[crcSize:] 620 if dec.maxSize < int64(len(twmBytes)) { 621 return nil, nil, DataCorruptionError{fmt.Errorf("length %d exceeded maximum possible value of %d bytes", int64(len(twmBytes)), dec.maxSize)} 622 } 623 624 // check checksum before decoding twmBytes 625 if len(twmBytes) == 0 { 626 return nil, nil, DataCorruptionError{fmt.Errorf("failed to read amino sized bytes: %w", err)} 627 } 628 actualCRC := crc32.Checksum(twmBytes, crc32c) 629 if actualCRC != crc { 630 return nil, nil, DataCorruptionError{fmt.Errorf("checksums do not match: read: %v, actual: %v", crc, actualCRC)} 631 } 632 633 // decode amino sized bytes. 634 res := new(TimedWALMessage) //nolint: gosimple 635 err = amino.UnmarshalSized(twmBytes, res) 636 if err != nil { 637 return nil, nil, DataCorruptionError{fmt.Errorf("failed to decode twmBytes: %w", err)} 638 } 639 640 return res, nil, err 641 } 642 643 type NopWAL struct{} 644 645 var _ WAL = NopWAL{} 646 647 func (NopWAL) SetLogger(l *slog.Logger) {} 648 func (NopWAL) Write(m WALMessage) error { return nil } 649 func (NopWAL) WriteSync(m WALMessage) error { return nil } 650 func (NopWAL) WriteMetaSync(m MetaMessage) error { return nil } 651 func (NopWAL) FlushAndSync() error { return nil } 652 func (NopWAL) SearchForHeight(height int64, options *WALSearchOptions) (rd io.ReadCloser, found bool, err error) { 653 return nil, false, nil 654 } 655 func (NopWAL) Start() error { return nil } 656 func (NopWAL) Stop() error { return nil } 657 func (NopWAL) Wait() {}