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  }