Sigrid Solveig HaflínudóttirClear(): don't leak the segment file descriptor4aa6fe22 years agoHistory
1049 lines · 27.1 KB · Go
RawDownload
package wal import (	"bytes"	"encoding/base64"	"encoding/binary"	"encoding/json"	"errors"	"fmt"	"io/ioutil"	"os"	"path/filepath"	"strconv"	"strings"	"sync"	"unicode/utf8"	"unsafe" 	"github.com/tidwall/gjson"	"github.com/tidwall/tinylru") var (	// ErrCorrupt is returns when the log is corrupt.	ErrCorrupt = errors.New("log corrupt") 	// ErrClosed is returned when an operation cannot be completed because	// the log is closed.	ErrClosed = errors.New("log closed") 	// ErrNotFound is returned when an entry is not found.	ErrNotFound = errors.New("not found") 	// ErrOutOfOrder is returned from Write() when the index is not equal to	// LastIndex()+1. It's required that log monotonically grows by one and has	// no gaps. Thus, the series 10,11,12,13,14 is valid, but 10,11,13,14 is	// not because there's a gap between 11 and 13. Also, 10,12,11,13 is not	// valid because 12 and 11 are out of order.	ErrOutOfOrder = errors.New("out of order") 	// ErrOutOfRange is returned from TruncateFront() and TruncateBack() when	// the index not in the range of the log's first and last index. Or, this	// may be returned when the caller is attempting to remove *all* entries;	// The log requires that at least one entry exists following a truncate.	ErrOutOfRange = errors.New("out of range") 	// ErrEmptyLog is returned by Open() when the `AllowEmpty` option was not	// provided and log has been emptied due to the use of TruncateFront() or	// TruncateBack().	ErrEmptyLog = errors.New("empty log")) // LogFormat is the format of the log files.type LogFormat byte const (	// Binary format writes entries in binary. This is the default and, unless	// a good reason otherwise, should be used in production.	Binary LogFormat = 0	// JSON format writes entries as JSON lines. This causes larger, human	// readable files.	JSON LogFormat = 1) // Options for Logtype Options struct {	// NoSync disables fsync after writes. This is less durable and puts the	// log at risk of data loss when there's a server crash.	NoSync bool	// SegmentSize of each segment. This is just a target value, actual size	// may differ. Default 20 MB	SegmentSize int	// LogFormat is the format of the log files. Default Binary	LogFormat LogFormat	// SegmentCacheSize is the maximum number of segments that will be held in	// memory for caching. Increasing this value may enhance performance for	// concurrent read operations. Default 1	SegmentCacheSize int	// NoCopy allows for the Read() operation to return the raw underlying data	// slice. This is an optimization to help minimize allocations. When this	// option is set, do not modify the returned data because it may affect	// other Read calls. Default false	NoCopy bool	// AllowEmpty allows for a log to have all entries removed through the use	// of TruncateFront() or TruncateBack(). Otherwise without this option,	// at least one entry must always remain following a truncate operation.	// Default false	//	// Warning: using this option changes the behavior of the log in the	// following ways:	// - An empty log will always have the FirstIndex() be equal to	//   LastIndex()+1.	// - For a newly created log that has no entries, FirstIndex() and	//   LastIndex() return 1 and 0, respectively.	//   Without AllowEmpty, both return 0.	AllowEmpty bool	// Perms represents the datafiles modes and permission bits	DirPerms  os.FileMode	FilePerms os.FileMode	// RecoverCorruptedTail will attempt to recover a corrupted tail in the last segment automatically.	RecoverCorruptedTail bool	// Logging function.	Logf func(fmt string, v ...any)} func noLog(fmt string, v ...any) {} // DefaultOptions for Open().var DefaultOptions = &Options{	NoSync:               false,    // Fsync after every write	SegmentSize:          20971520, // 20 MB log segment files.	LogFormat:            Binary,   // Binary format is small and fast.	SegmentCacheSize:     2,        // Number of cached in-memory segments	NoCopy:               false,    // Make a new copy of data for every Read call.	DirPerms:             0750,     // Permissions for the created directories	FilePerms:            0640,     // Permissions for the created data files	RecoverCorruptedTail: false,    // Don't recover corrupted tail.	Logf:                 noLog,} // Log represents a write ahead logtype Log struct {	mu         sync.RWMutex	path       string      // absolute path to log directory	opts       Options     // log options	closed     bool        // log is closed	corrupt    bool        // log may be corrupt	segments   []*segment  // all known log segments	firstIndex uint64      // index of the first entry in log	lastIndex  uint64      // index of the last entry in log	sfile      *os.File    // tail segment file handle	dir        *os.File    // dir for syncing	wbatch     Batch       // reusable write batch	scache     tinylru.LRU // segment entries cache 	logf func(fmt string, v ...any)} // segment represents a single segment file.type segment struct {	path  string // path of segment file	index uint64 // first index of segment	ebuf  []byte // cached entries buffer	epos  []bpos // cached entries positions in buffer} type bpos struct {	pos int // byte position	end int // one byte past pos} // Open a new write ahead logfunc Open(path string, opts *Options) (*Log, error) {	if opts == nil {		opts = DefaultOptions	}	if opts.SegmentCacheSize <= 0 {		opts.SegmentCacheSize = DefaultOptions.SegmentCacheSize	}	if opts.SegmentSize <= 0 {		opts.SegmentSize = DefaultOptions.SegmentSize	}	if opts.DirPerms == 0 {		opts.DirPerms = DefaultOptions.DirPerms	}	if opts.FilePerms == 0 {		opts.FilePerms = DefaultOptions.FilePerms	} 	var err error	path, err = abs(path)	if err != nil {		return nil, err	}	l := &Log{path: path, opts: *opts}	if l.logf = opts.Logf; l.logf == nil {		l.logf = DefaultOptions.Logf	}	l.scache.Resize(l.opts.SegmentCacheSize)	if err := os.MkdirAll(path, l.opts.DirPerms); err != nil {		return nil, err	} else if l.dir, err = os.OpenFile(path, os.O_RDONLY, 0); err != nil {		return nil, err	}	if err := l.load(); err != nil {		return nil, err	}	return l, nil} func abs(path string) (string, error) {	if path == ":memory:" {		return "", errors.New("in-memory log not supported")	}	return filepath.Abs(path)} func (l *Log) pushCache(segIdx int) {	_, _, _, v, evicted :=		l.scache.SetEvicted(segIdx, l.segments[segIdx])	if evicted {		s := v.(*segment)		s.ebuf = nil		s.epos = nil	}} // load all the segments. This operation also cleans up any START/END segments.func (l *Log) load() error {	fis, err := ioutil.ReadDir(l.path)	if err != nil {		return err	}	startIdx := -1	endIdx := -1	for _, fi := range fis {		name := fi.Name()		if fi.IsDir() || len(name) < 20 {			continue		}		index, err := strconv.ParseUint(name[:20], 10, 64)		if err != nil || index == 0 {			continue		}		isStart := len(name) == 26 && strings.HasSuffix(name, ".START")		isEnd := len(name) == 24 && strings.HasSuffix(name, ".END")		if len(name) == 20 || isStart || isEnd {			if isStart {				startIdx = len(l.segments)			} else if isEnd && endIdx == -1 {				endIdx = len(l.segments)			}			l.segments = append(l.segments, &segment{				index: index,				path:  filepath.Join(l.path, name),			})		}	}	if len(l.segments) == 0 {		// Create a new log		l.segments = append(l.segments, &segment{			index: 1,			path:  filepath.Join(l.path, segmentName(1)),		})		l.firstIndex = 1		l.lastIndex = 0		l.sfile, err = os.OpenFile(l.segments[0].path,			os.O_CREATE|os.O_RDWR|os.O_TRUNC, l.opts.FilePerms)		return err	}	// Open existing log. Clean up log if START of END segments exists.	if startIdx != -1 {		if endIdx != -1 {			// There should not be a START and END at the same time			return ErrCorrupt		}		// Delete all files leading up to START		for i := 0; i < startIdx; i++ {			if err := os.Remove(l.segments[i].path); err != nil {				return err			}		}		l.segments = append([]*segment{}, l.segments[startIdx:]...)		// Rename the START segment		orgPath := l.segments[0].path		finalPath := orgPath[:len(orgPath)-len(".START")]		err := os.Rename(orgPath, finalPath)		if err != nil {			return err		}		l.segments[0].path = finalPath	}	if endIdx != -1 {		// Delete all files following END		for i := len(l.segments) - 1; i > endIdx; i-- {			if err := os.Remove(l.segments[i].path); err != nil {				return err			}		}		l.segments = append([]*segment{}, l.segments[:endIdx+1]...)		if len(l.segments) > 1 && l.segments[len(l.segments)-2].index ==			l.segments[len(l.segments)-1].index {			// remove the segment prior to the END segment because it shares			// the same starting index.			l.segments[len(l.segments)-2] = l.segments[len(l.segments)-1]			l.segments = l.segments[:len(l.segments)-1]		}		// Rename the END segment		orgPath := l.segments[len(l.segments)-1].path		finalPath := orgPath[:len(orgPath)-len(".END")]		err := os.Rename(orgPath, finalPath)		if err != nil {			return err		}		l.segments[len(l.segments)-1].path = finalPath	}	l.firstIndex = l.segments[0].index	// Open the last segment for appending	lseg := l.segments[len(l.segments)-1]	// Load the last segment entries	if err := l.loadSegmentEntries(lseg, l.opts.RecoverCorruptedTail); err != nil {		return err	}	l.sfile, err = os.OpenFile(lseg.path, os.O_WRONLY, l.opts.FilePerms)	if err != nil {		return err	}	if _, err := l.sfile.Seek(int64(len(lseg.ebuf)), 0); err != nil {		return err	}	l.lastIndex = lseg.index + uint64(len(lseg.epos)) - 1	if l.lastIndex > 0 && l.firstIndex > l.lastIndex && !l.opts.AllowEmpty {		return ErrEmptyLog	}	return nil} // segmentName returns a 20-byte textual representation of an index// for lexical ordering. This is used for the file names of log segments.func segmentName(index uint64) string {	return fmt.Sprintf("%020d", index)} // Close the log.func (l *Log) Close() error {	l.mu.Lock()	defer l.mu.Unlock()	if l.closed {		if l.corrupt {			return ErrCorrupt		}		return ErrClosed	}	if err := l.sfile.Sync(); err != nil {		return err	}	if err := l.sfile.Close(); err != nil {		return err	}	if err := l.dir.Close(); err != nil {		return err	}	l.closed = true	if l.corrupt {		return ErrCorrupt	}	return nil} // Write an entry to the log.func (l *Log) Write(index uint64, data []byte) error {	l.mu.Lock()	defer l.mu.Unlock()	if l.corrupt {		return ErrCorrupt	} else if l.closed {		return ErrClosed	}	l.wbatch.Clear()	l.wbatch.Write(index, data)	return l.writeBatch(&l.wbatch)} func (l *Log) appendEntry(dst []byte, index uint64, data []byte) (out []byte,	epos bpos) {	if l.opts.LogFormat == JSON {		return appendJSONEntry(dst, index, data)	}	return appendBinaryEntry(dst, data)} // Cycle the old segment for a new segment.func (l *Log) cycle() error {	if err := l.sfile.Sync(); err != nil {		return err	}	if err := l.sfile.Close(); err != nil {		return err	}	if err := l.dir.Sync(); err != nil {		return err	}	// cache the previous segment	l.pushCache(len(l.segments) - 1)	s := &segment{		index: l.lastIndex + 1,		path:  filepath.Join(l.path, segmentName(l.lastIndex+1)),	}	var err error	l.sfile, err = os.OpenFile(s.path, os.O_CREATE|os.O_RDWR|os.O_TRUNC,		l.opts.FilePerms)	if err != nil {		return err	}	l.segments = append(l.segments, s)	return nil} func appendJSONEntry(dst []byte, index uint64, data []byte) (out []byte,	epos bpos) {	// {"index":number,"data":string}	pos := len(dst)	dst = append(dst, `{"index":"`...)	dst = strconv.AppendUint(dst, index, 10)	dst = append(dst, `","data":`...)	dst = appendJSONData(dst, data)	dst = append(dst, '}', '\n')	return dst, bpos{pos, len(dst)}} func appendJSONData(dst []byte, s []byte) []byte {	if utf8.Valid(s) {		b, _ := json.Marshal(*(*string)(unsafe.Pointer(&s)))		dst = append(dst, '"', '+')		return append(dst, b[1:]...)	}	dst = append(dst, '"', '$')	dst = append(dst, base64.URLEncoding.EncodeToString(s)...)	return append(dst, '"')} func appendBinaryEntry(dst []byte, data []byte) (out []byte, epos bpos) {	// data_size + data	pos := len(dst)	dst = appendUvarint(dst, uint64(len(data)))	dst = append(dst, data...)	return dst, bpos{pos, len(dst)}} func appendUvarint(dst []byte, x uint64) []byte {	var buf [10]byte	n := binary.PutUvarint(buf[:], x)	dst = append(dst, buf[:n]...)	return dst} // Batch of entries. Used to write multiple entries at once using WriteBatch().type Batch struct {	entries []batchEntry	datas   []byte} type batchEntry struct {	index uint64	size  int} // Write an entry to the batchfunc (b *Batch) Write(index uint64, data []byte) {	b.entries = append(b.entries, batchEntry{index, len(data)})	b.datas = append(b.datas, data...)} // Clear the batch for reuse.func (b *Batch) Clear() {	b.entries = b.entries[:0]	b.datas = b.datas[:0]} // WriteBatch writes the entries in the batch to the log in the order that they// were added to the batch. The batch is cleared upon a successful return.func (l *Log) WriteBatch(b *Batch) error {	l.mu.Lock()	defer l.mu.Unlock()	if l.corrupt {		return ErrCorrupt	} else if l.closed {		return ErrClosed	}	if len(b.entries) == 0 {		return nil	}	return l.writeBatch(b)} func (l *Log) writeBatch(b *Batch) error {	// check that all indexes in batch are sane	for i := 0; i < len(b.entries); i++ {		if b.entries[i].index != l.lastIndex+uint64(i+1) {			return ErrOutOfOrder		}	}	// load the tail segment	s := l.segments[len(l.segments)-1]	if len(s.ebuf) > l.opts.SegmentSize {		// tail segment has reached capacity. Close it and create a new one.		if err := l.cycle(); err != nil {			return err		}		s = l.segments[len(l.segments)-1]	} 	mark := len(s.ebuf)	datas := b.datas	for i := 0; i < len(b.entries); i++ {		data := datas[:b.entries[i].size]		var epos bpos		s.ebuf, epos = l.appendEntry(s.ebuf, b.entries[i].index, data)		s.epos = append(s.epos, epos)

Lines 1–500 of 1049.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t