github.com/scottcagno/storage@v1.8.0/pkg/lsmt/wal/wal.go (about) 1 package wal 2 3 import ( 4 "errors" 5 "fmt" 6 "github.com/scottcagno/storage/pkg/lsmt/binary" 7 "io" 8 "os" 9 "path/filepath" 10 "runtime" 11 "strconv" 12 "strings" 13 "sync" 14 ) 15 16 const ( 17 FilePrefix = "dat-" 18 FileSuffix = ".seg" 19 defaultMaxFileSize int64 = 16 << 10 // 16 KB 20 defaultBasePath = "log" 21 defaultSyncOnWrite = false 22 remainingTrigger = 64 23 ) 24 25 var ( 26 maxFileSize = defaultMaxFileSize 27 ErrOutOfBounds = errors.New("error: out of bounds") 28 ErrSegmentFull = errors.New("error: segment is full") 29 ErrFileClosed = errors.New("error: file closed") 30 ErrBadArgument = errors.New("error: bad argument") 31 ErrNoPathProvided = errors.New("error: no path provided") 32 ErrOptionsMissing = errors.New("error: options missing") 33 ) 34 35 // segEntry contains the metadata for a single segEntry within the file segment 36 type segEntry struct { 37 index int64 // index is the "id" of this segEntry 38 offset int64 // offset is the actual offset of this segEntry in the segment file 39 } 40 41 // String is the stringer method for an segEntry 42 func (e segEntry) String() string { 43 return fmt.Sprintf("segEntry.index=%d, segEntry.offset=%d", e.index, e.offset) 44 } 45 46 // segment contains the metadata for the file segment 47 type segment struct { 48 path string // path is the full path to this segment file 49 index int64 // starting index of the segment 50 entries []segEntry // entries is an index of the entries in the segment 51 remaining int64 // remaining is the bytes left after max file size minus segEntry data 52 } 53 54 // String is the stringer method for a segment 55 func (s *segment) String() string { 56 var ss string 57 ss += fmt.Sprintf("path: %q\n", filepath.Base(s.path)) 58 ss += fmt.Sprintf("index: %d\n", s.index) 59 ss += fmt.Sprintf("entries: %d\n", len(s.entries)) 60 ss += fmt.Sprintf("remaining: %d\n", s.remaining) 61 return ss 62 } 63 64 func MakeFileNameFromIndex(index int64) string { 65 hexa := strconv.FormatInt(index, 16) 66 return fmt.Sprintf("%s%010s%s", FilePrefix, hexa, FileSuffix) 67 } 68 69 func GetIndexFromFileName(name string) (int64, error) { 70 hexa := name[len(FilePrefix) : len(name)-len(FileSuffix)] 71 return strconv.ParseInt(hexa, 16, 32) 72 } 73 74 // getFirstIndex returns the first index in the entries list 75 func (s *segment) getFirstIndex() int64 { 76 return s.index 77 } 78 79 // getLastIndex returns the last index in the entries list 80 func (s *segment) getLastIndex() int64 { 81 if len(s.entries) > 0 { 82 return s.entries[len(s.entries)-1].index 83 } 84 return s.index 85 } 86 87 // findEntryIndex performs binary search to find the segEntry containing provided index 88 func (s *segment) findEntryIndex(index int64) int { 89 // declare for later 90 i, j := 0, len(s.entries) 91 // otherwise, perform binary search 92 for i < j { 93 h := i + (j-i)/2 94 if index >= s.entries[h].index { 95 i = h + 1 96 } else { 97 j = h 98 } 99 } 100 return i - 1 101 } 102 103 var defaultWALConfig = &WALConfig{ 104 BasePath: defaultBasePath, 105 MaxFileSize: defaultMaxFileSize, 106 SyncOnWrite: defaultSyncOnWrite, 107 } 108 109 type WALConfig struct { 110 BasePath string // base storage path 111 MaxFileSize int64 // memtable flush threshold in KB 112 SyncOnWrite bool // perform sync every time an entry is write 113 } 114 115 func checkWALConfig(conf *WALConfig) *WALConfig { 116 if conf == nil { 117 return defaultWALConfig 118 } 119 if conf.BasePath == *new(string) { 120 conf.BasePath = defaultBasePath 121 } 122 if conf.MaxFileSize < 1 { 123 conf.MaxFileSize = defaultMaxFileSize 124 } 125 return conf 126 } 127 128 // WAL is a write-ahead log structure 129 type WAL struct { 130 lock sync.RWMutex // lock is a mutual exclusion lock 131 conf *WALConfig 132 r *binary.Reader // r is a binary reader 133 w *binary.Writer // w is a binary writer 134 firstIndex int64 // firstIndex is the index of the first segEntry 135 lastIndex int64 // lastIndex is the index of the last segEntry 136 segments []*segment // segments is an index of the current file segments 137 active *segment // active is the current active segment 138 } 139 140 // OpenWAL opens and returns a new write-ahead log structure 141 func OpenWAL(c *WALConfig) (*WAL, error) { 142 // check config 143 conf := checkWALConfig(c) 144 // TODO: consider replacing `filepath.Abs()`, and `filepath.ToSlash()` 145 // TODO: with `filepath.Clean()` at some point or another. It should 146 // TODO: close enough to the same (possibly even better), so yeah. 147 // make sure we are working with absolute paths 148 base, err := filepath.Abs(conf.BasePath) 149 if err != nil { 150 return nil, err 151 } 152 // sanitize any path separators 153 base = filepath.ToSlash(base) 154 // create any directories if they are not there 155 err = os.MkdirAll(base, os.ModeDir) 156 if err != nil { 157 return nil, err 158 } 159 // create a new write-ahead log instance 160 l := &WAL{ 161 conf: conf, 162 firstIndex: 0, 163 lastIndex: 1, 164 segments: make([]*segment, 0), 165 } 166 // attempt to load segments 167 err = l.loadIndex() 168 if err != nil { 169 return nil, err 170 } 171 // return write-ahead log 172 return l, nil 173 } 174 175 func (l *WAL) CloseAndRemove() error { 176 // lock 177 l.lock.Lock() 178 defer l.lock.Unlock() 179 // sync and close writer 180 err := l.w.Close() 181 if err != nil { 182 return err 183 } 184 // close reader 185 err = l.r.Close() 186 if err != nil { 187 return err 188 } 189 // reset the segments 190 l.segments = make([]*segment, 0) 191 // reset first and last index 192 l.firstIndex = 0 193 l.lastIndex = 1 194 // erase all files 195 err = os.RemoveAll(l.conf.BasePath) 196 if err != nil { 197 return err 198 } 199 return nil 200 } 201 202 // loadIndex initializes the segment index. It looks for segment 203 // files in the base directory and attempts to index the segment as 204 // well as any of the entries within the segment. If this is a new 205 // instance, it will create a new segment that is ready for writing. 206 func (l *WAL) loadIndex() error { 207 // lock 208 l.lock.Lock() 209 defer l.lock.Unlock() 210 // get the files in the base directory path 211 files, err := os.ReadDir(l.conf.BasePath) 212 if err != nil { 213 return err 214 } 215 // list the files in the base directory path and attempt to index the entries 216 for _, file := range files { 217 // skip non data files 218 if file.IsDir() || 219 !strings.HasPrefix(file.Name(), FilePrefix) || 220 !strings.HasSuffix(file.Name(), FileSuffix) { 221 continue // skip this, continue on to the next file 222 } 223 // check the size of segment file 224 fi, err := file.Info() 225 if err != nil { 226 return err 227 } 228 // if the file is empty, remove it and skip to next file 229 if fi.Size() == 0 { 230 err = os.Remove(filepath.Join(l.conf.BasePath, file.Name())) 231 if err != nil { 232 return err 233 } 234 continue // make sure we skip to next segment 235 } 236 // attempt to load segment (and index entries in segment) 237 s, err := l.loadSegmentFile(filepath.Join(l.conf.BasePath, file.Name())) 238 if err != nil { 239 return err 240 } 241 // segment has been loaded successfully, append to the segments list 242 l.segments = append(l.segments, s) 243 } 244 // check to see if any segments were found. If not, initialize a new one 245 if len(l.segments) == 0 { 246 // create a new segment file 247 s, err := l.makeSegmentFile(l.lastIndex) 248 if err != nil { 249 return err 250 } 251 // segment has been created successfully, append to the segments list 252 l.segments = append(l.segments, s) 253 } 254 // segments have either been loaded or created, so now we 255 // should go about updating the active segment pointer to 256 // point to the "tail" (the last segment in the segment list) 257 l.active = l.getLastSegment() 258 // we should be good to go, lets attempt to open a file 259 // reader to work with the active segment 260 l.r, err = binary.OpenReader(l.active.path) 261 if err != nil { 262 return err 263 } 264 // and then attempt to open a file writer to also work 265 // with the active segment, so we can begin appending data 266 l.w, err = binary.OpenWriterWithSync(l.active.path, l.conf.SyncOnWrite) 267 if err != nil { 268 return err 269 } 270 // finally, update the firstIndex and lastIndex 271 l.firstIndex = l.segments[0].index 272 // and update last index 273 l.lastIndex = l.getLastSegment().getLastIndex() 274 return nil 275 } 276 277 // loadSegment attempts to open the segment file at the path provided 278 // and index the entries within the segment. It will return an os.PathError 279 // if the file does not exist, an io.ErrUnexpectedEOF if the file exists 280 // but is empty and has no data to read, and ErrSegmentFull if the file 281 // has met the maxFileSize. It will return the segment and nil error on success. 282 func (l *WAL) loadSegmentFile(path string) (*segment, error) { 283 // check to make sure path exists before continuing 284 _, err := os.Stat(path) 285 if err != nil { 286 return nil, err 287 } 288 // attempt to open existing segment file for reading 289 fd, err := os.OpenFile(path, os.O_RDONLY, 0666) 290 if err != nil { 291 return nil, err 292 } 293 // defer file close 294 defer func(fd *os.File) { 295 _ = fd.Close() 296 }(fd) 297 // create a new segment to append indexed entries to 298 s := &segment{ 299 path: path, 300 entries: make([]segEntry, 0), 301 } 302 // read segment file and index entries 303 index, err := GetIndexFromFileName(filepath.Base(fd.Name())) 304 if err != nil { 305 return nil, err 306 } 307 for { 308 // get the current offset of the 309 // reader for the segEntry later 310 offset, err := binary.Offset(fd) 311 if err != nil { 312 return nil, err 313 } 314 // read and decode segEntry 315 _, err = binary.DecodeEntry(fd) 316 if err != nil { 317 if err == io.EOF || err == io.ErrUnexpectedEOF { 318 break 319 } 320 return nil, err 321 } 322 // get current offset 323 // add segEntry index to segment entries list 324 s.entries = append(s.entries, segEntry{ 325 index: index, 326 offset: offset, 327 }) 328 // continue to process the next segEntry 329 index++ 330 } 331 // make sure to fill out the segment index from the first segEntry index 332 s.index = s.entries[0].index 333 // get the offset of the reader to calculate bytes remaining 334 offset, err := binary.Offset(fd) 335 if err != nil { 336 return nil, err 337 } 338 // update the segment remaining bytes 339 s.remaining = maxFileSize - offset 340 return s, nil 341 } 342 343 // makeSegment attempts to make a new segment automatically using the timestamp 344 // as the segment name. On success, it will simply return a new segment and a nil error 345 func (l *WAL) makeSegmentFile(index int64) (*segment, error) { 346 // create a new file 347 path := filepath.Join(l.conf.BasePath, MakeFileNameFromIndex(index)) 348 fd, err := os.Create(path) 349 if err != nil { 350 return nil, err 351 } 352 // don't forget to close it 353 err = fd.Close() 354 if err != nil { 355 return nil, err 356 } 357 // create and return new segment 358 s := &segment{ 359 path: path, 360 index: l.lastIndex, 361 entries: make([]segEntry, 0), 362 remaining: l.conf.MaxFileSize, 363 } 364 return s, nil 365 } 366 367 // findSegmentIndex performs binary search to find the segment containing provided index 368 func (l *WAL) findSegmentIndex(index int64) int { 369 // declare for later 370 i, j := 0, len(l.segments) 371 // otherwise, perform binary search 372 for i < j { 373 h := i + (j-i)/2 374 if index >= l.segments[h].index { 375 i = h + 1 376 } else { 377 j = h 378 } 379 } 380 return i - 1 381 } 382 383 // getLastSegment returns the tail segment in the segments index list 384 func (l *WAL) getLastSegment() *segment { 385 return l.segments[len(l.segments)-1] 386 } 387 388 // cycleSegment adds a new segment to replace the current (active) segment 389 func (l *WAL) cycleSegment() error { 390 // sync and close current file segment 391 err := l.w.Close() 392 if err != nil { 393 return err 394 } 395 // create a new segment file 396 s, err := l.makeSegmentFile(l.lastIndex) 397 if err != nil { 398 return err 399 } 400 // add segment to segment index list 401 l.segments = append(l.segments, s) 402 // update the active segment pointer 403 l.active = l.getLastSegment() 404 // open file writer associated with active segment 405 l.w, err = binary.OpenWriterWithSync(l.active.path, l.conf.SyncOnWrite) 406 if err != nil { 407 return err 408 } 409 // update file reader associated with the active segment 410 l.r, err = binary.OpenReader(l.active.path) 411 if err != nil { 412 return err 413 } 414 return nil 415 } 416 417 // Read reads an segEntry from the write-ahead log at the specified index 418 func (l *WAL) Read(index int64) (*binary.Entry, error) { 419 // read lock 420 l.lock.RLock() 421 defer l.lock.RUnlock() 422 // error checking 423 if index < l.firstIndex || index > l.lastIndex { 424 return nil, ErrOutOfBounds 425 } 426 var err error 427 // find the segment containing the provided index 428 s := l.segments[l.findSegmentIndex(index)] 429 // make sure we are reading from the correct file 430 l.r, err = l.r.ReadFrom(s.path) 431 if err != nil { 432 return nil, err 433 } 434 // find the offset for the segEntry containing the provided index 435 offset := s.entries[s.findEntryIndex(index)].offset 436 // read segEntry at offset 437 e, err := l.r.ReadEntryAt(offset) 438 if err != nil { 439 return nil, err 440 } 441 return e, nil 442 } 443 444 // Write writes an segEntry to the write-ahead log in an append-only fashion 445 func (l *WAL) Write(e *binary.Entry) (int64, error) { 446 // lock 447 l.lock.Lock() 448 defer l.lock.Unlock() 449 // write segEntry 450 offset, err := l.w.WriteEntry(e) 451 if err != nil { 452 return 0, err 453 } 454 // add new segEntry to the segment index 455 l.active.entries = append(l.active.entries, segEntry{ 456 index: l.lastIndex, 457 offset: offset, 458 }) 459 // update lastIndex 460 l.lastIndex++ 461 // grab the current offset written 462 offset2, err := l.w.Offset() 463 if err != nil { 464 return 0, err 465 } 466 // update segment remaining 467 l.active.remaining -= offset2 - offset 468 // check to see if the active segment needs to be cycled 469 if l.active.remaining < remainingTrigger { 470 err = l.cycleSegment() 471 if err != nil { 472 return 0, err 473 } 474 } 475 return l.lastIndex - 1, nil 476 } 477 478 // WriteBatch writes a batch of entries performing no syncing until the end of the batch 479 func (l *WAL) WriteBatch(batch *binary.Batch) error { 480 // lock 481 l.lock.Lock() 482 defer l.lock.Unlock() 483 // check sync policy 484 changedSyncPolicy := false 485 if l.conf.SyncOnWrite == true { 486 l.conf.SyncOnWrite = false // if it's on, temporarily disable 487 l.w.SetSyncOnWrite(false) 488 changedSyncPolicy = true 489 } 490 // iterate batch 491 for i := range batch.Entries { 492 // entry 493 e := batch.Entries[i] 494 // write entry to data file 495 offset, err := l.w.WriteEntry(e) 496 if err != nil { 497 return err 498 } 499 // add new segEntry to the segment index 500 l.active.entries = append(l.active.entries, segEntry{ 501 index: l.lastIndex, 502 offset: offset, 503 }) 504 // update lastIndex 505 l.lastIndex++ 506 // grab the current offset written 507 offset2, err := l.w.Offset() 508 if err != nil { 509 return err 510 } 511 // update segment remaining 512 l.active.remaining -= offset2 - offset 513 // check to see if the active segment needs to be cycled 514 if l.active.remaining < remainingTrigger { 515 err = l.cycleSegment() 516 if err != nil { 517 return err 518 } 519 } 520 } 521 // after batch, set everything back how it was 522 if changedSyncPolicy { 523 l.conf.SyncOnWrite = true 524 l.w.SetSyncOnWrite(true) 525 } 526 // after batch has been written, do sync 527 err := l.w.Sync() 528 if err != nil { 529 return err 530 } 531 return nil 532 } 533 534 // Scan provides an iterator method for the write-ahead log 535 func (l *WAL) Scan(iter func(e *binary.Entry) bool) error { 536 // lock 537 l.lock.Lock() 538 defer l.lock.Unlock() 539 // init for any errors 540 var err error 541 // range the segment index 542 for _, sidx := range l.segments { 543 //fmt.Printf("segment: %s\n", sidx) 544 // make sure we are reading the right data 545 l.r, err = l.r.ReadFrom(sidx.path) 546 if err != nil { 547 return err 548 } 549 // range the segment entries index 550 for _, eidx := range sidx.entries { 551 // read segEntry 552 e, err := l.r.ReadEntryAt(eidx.offset) 553 if err != nil { 554 if err == io.EOF || err == io.ErrUnexpectedEOF { 555 break 556 } 557 return err 558 } 559 // check segEntry against iterator boolean function 560 if !iter(e) { 561 // if it returns false, then process next segEntry 562 continue 563 } 564 } 565 // outside segEntry loop 566 } 567 // outside segment loop 568 return nil 569 } 570 571 // TruncateFront removes all segments and entries before specified index 572 func (l *WAL) TruncateFront(index int64) error { 573 // lock 574 l.lock.Lock() 575 defer l.lock.Unlock() 576 // perform bounds check 577 if index == 0 || 578 l.lastIndex == 0 || 579 index < l.firstIndex || index > l.lastIndex { 580 return ErrOutOfBounds 581 } 582 if index == l.firstIndex { 583 return nil // nothing to truncate 584 } 585 // locate segment in segment index list containing specified index 586 sidx := l.findSegmentIndex(index) 587 // isolate whole segments that can be removed 588 for i := 0; i < sidx; i++ { 589 // remove segment file 590 err := os.Remove(l.segments[i].path) 591 if err != nil { 592 return err 593 } 594 } 595 // remove segments from segment index (cut, i-j) 596 i, j := 0, sidx 597 copy(l.segments[i:], l.segments[j:]) 598 for k, n := len(l.segments)-j+i, len(l.segments); k < n; k++ { 599 l.segments[k] = nil // or the zero value of T 600 } 601 l.segments = l.segments[:len(l.segments)-j+i] 602 // update firstIndex 603 l.firstIndex = l.segments[0].index 604 // prepare to re-write partial segment 605 var err error 606 var entries []segEntry 607 tmpfd, err := os.Create(filepath.Join(l.conf.BasePath, "tmp-partial.seg")) 608 if err != nil { 609 return err 610 } 611 // after the segment index cut, segment 0 will 612 // contain the partials that we must re-write 613 if l.segments[0].index < index { 614 // make sure we are reading from the correct path 615 l.r, err = l.r.ReadFrom(l.segments[0].path) 616 if err != nil { 617 return err 618 } 619 // range the entries within this segment to find 620 // the ones that are greater than the index and 621 // write those to a temporary buffer.... 622 for _, ent := range l.segments[0].entries { 623 if ent.index < index { 624 continue // skip 625 } 626 // read segEntry 627 e, err := l.r.ReadEntryAt(ent.offset) 628 if err != nil { 629 return err 630 } 631 // write segEntry to temp file 632 ent.offset, err = binary.EncodeEntry(tmpfd, e) 633 if err != nil { 634 return err 635 } 636 // sync write 637 err = tmpfd.Sync() 638 if err != nil { 639 return err 640 } 641 // append to a new entries list 642 entries = append(entries, ent) 643 } 644 // move reader back to active segment file 645 l.r, err = l.r.ReadFrom(l.active.path) 646 if err != nil { 647 return err 648 } 649 // close temp file 650 err = tmpfd.Close() 651 if err != nil { 652 return err 653 } 654 // remove partial segment file 655 err = os.Remove(l.segments[0].path) 656 if err != nil { 657 return err 658 } 659 // change temp file name 660 err = os.Rename(tmpfd.Name(), l.segments[0].path) 661 if err != nil { 662 return err 663 } 664 // update segment 665 l.segments[0].entries = entries 666 l.segments[0].index = entries[0].index 667 } 668 return nil 669 } 670 671 func (l *WAL) GetConfig() *WALConfig { 672 // lock 673 l.lock.Lock() 674 defer l.lock.Unlock() 675 return l.conf 676 } 677 678 func (l *WAL) Sync() error { 679 // lock 680 l.lock.Lock() 681 defer l.lock.Unlock() 682 err := l.w.Sync() 683 if err != nil { 684 return err 685 } 686 return nil 687 } 688 689 // Count returns the number of entries currently in the write-ahead log 690 func (l *WAL) Count() int { 691 // lock 692 l.lock.Lock() 693 defer l.lock.Unlock() 694 // get count 695 var count int 696 for _, s := range l.segments { 697 count += len(s.entries) 698 } 699 // return count 700 return count 701 } 702 703 // FirstIndex returns the write-ahead logs first index 704 func (l *WAL) FirstIndex() int64 { 705 // lock 706 l.lock.Lock() 707 defer l.lock.Unlock() 708 return l.firstIndex 709 } 710 711 // LastIndex returns the write-ahead logs first index 712 func (l *WAL) LastIndex() int64 { 713 // lock 714 l.lock.Lock() 715 defer l.lock.Unlock() 716 return l.lastIndex 717 } 718 719 // Close syncs and closes the write-ahead log 720 func (l *WAL) Close() error { 721 // lock 722 l.lock.Lock() 723 defer l.lock.Unlock() 724 // sync and close writer 725 err := l.w.Close() 726 if err != nil { 727 return err 728 } 729 // close reader 730 err = l.r.Close() 731 if err != nil { 732 return err 733 } 734 // clean everything else up 735 l.r = nil 736 l.w = nil 737 l.firstIndex = 0 738 l.lastIndex = 0 739 l.segments = nil 740 l.active = nil 741 // force gc for good measure 742 runtime.GC() 743 return nil 744 } 745 746 // String is the stringer method for the write-ahead log 747 func (l *WAL) String() string { 748 var ss string 749 ss += fmt.Sprintf("\n\n[write-ahead log]\n") 750 ss += fmt.Sprintf("base: %q\n", l.conf.BasePath) 751 ss += fmt.Sprintf("firstIndex: %d\n", l.firstIndex) 752 ss += fmt.Sprintf("lastIndex: %d\n", l.lastIndex) 753 ss += fmt.Sprintf("segments: %d\n", len(l.segments)) 754 if l.active != nil { 755 ss += fmt.Sprintf("active: %q\n", filepath.Base(l.active.path)) 756 } 757 if len(l.segments) > 0 { 758 for i, s := range l.segments { 759 ss += fmt.Sprintf("segment[%d]:\n", i) 760 ss += fmt.Sprintf("\tpath: %q\n", filepath.Base(s.path)) 761 ss += fmt.Sprintf("\tindex: %d\n", s.index) 762 ss += fmt.Sprintf("\tentries: %d\n", len(s.entries)) 763 ss += fmt.Sprintf("\tremaining: %d\n", s.remaining) 764 } 765 } 766 ss += "\n" 767 return ss 768 }