tendermint/group.go

600 lines
13 KiB
Go
Raw Normal View History

2016-10-26 16:23:19 -07:00
package autofile
import (
"bufio"
2016-11-05 17:58:50 -07:00
"errors"
2016-10-26 16:23:19 -07:00
"io"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"time"
2016-11-05 17:58:50 -07:00
. "github.com/tendermint/go-common"
2016-10-26 16:23:19 -07:00
)
/*
You can open a Group to keep restrictions on an AutoFile, like
the maximum size of each chunk, and/or the total amount of bytes
stored in the group.
2016-10-26 22:11:43 -07:00
The first file to be written in the Group.Dir is the head file.
Dir/
- <HeadPath>
Once the Head file reaches the size limit, it will be rotated.
Dir/
- <HeadPath>.000 // First rolled file
- <HeadPath> // New head path, starts empty.
// The implicit index is 001.
As more files are written, the index numbers grow...
Dir/
- <HeadPath>.000 // First rolled file
- <HeadPath>.001 // Second rolled file
- ...
- <HeadPath> // New head path
The Group can also be used to binary-search for some line,
assuming that marker lines are written occasionally.
2016-10-26 16:23:19 -07:00
*/
const groupCheckDuration = 1000 * time.Millisecond
2016-10-26 21:50:07 -07:00
const defaultHeadSizeLimit = 10 * 1024 * 1024 // 10MB
2016-10-26 16:23:19 -07:00
type Group struct {
ID string
Head *AutoFile // The head AutoFile to write to
headBuf *bufio.Writer
Dir string // Directory that contains .Head
2016-10-26 16:23:19 -07:00
ticker *time.Ticker
mtx sync.Mutex
headSizeLimit int64
totalSizeLimit int64
2016-10-26 21:50:07 -07:00
minIndex int // Includes head
maxIndex int // Includes head, where Head will move to
2016-11-05 17:58:50 -07:00
// TODO: When we start deleting files, we need to start tracking GroupReaders
// and their dependencies.
2016-10-26 16:23:19 -07:00
}
func OpenGroup(head *AutoFile) (g *Group, err error) {
dir := path.Dir(head.Path)
g = &Group{
2016-10-26 21:50:07 -07:00
ID: "group:" + head.ID,
Head: head,
headBuf: bufio.NewWriterSize(head, 4096*10),
2016-10-26 21:50:07 -07:00
Dir: dir,
ticker: time.NewTicker(groupCheckDuration),
headSizeLimit: defaultHeadSizeLimit,
minIndex: 0,
maxIndex: 0,
2016-10-26 16:23:19 -07:00
}
2016-10-26 21:50:07 -07:00
gInfo := g.readGroupInfo()
g.minIndex = gInfo.MinIndex
g.maxIndex = gInfo.MaxIndex
2016-10-26 16:23:19 -07:00
go g.processTicks()
return
}
func (g *Group) SetHeadSizeLimit(limit int64) {
g.mtx.Lock()
g.headSizeLimit = limit
g.mtx.Unlock()
}
func (g *Group) HeadSizeLimit() int64 {
g.mtx.Lock()
defer g.mtx.Unlock()
return g.headSizeLimit
}
func (g *Group) SetTotalSizeLimit(limit int64) {
g.mtx.Lock()
g.totalSizeLimit = limit
g.mtx.Unlock()
}
func (g *Group) TotalSizeLimit() int64 {
g.mtx.Lock()
defer g.mtx.Unlock()
return g.totalSizeLimit
}
2016-10-26 21:50:07 -07:00
func (g *Group) MaxIndex() int {
g.mtx.Lock()
defer g.mtx.Unlock()
return g.maxIndex
}
2016-10-28 13:56:31 -07:00
// Auto appends "\n"
// NOTE: Writes are buffered so they don't write synchronously
2016-10-28 13:56:31 -07:00
// TODO: Make it halt if space is unavailable
func (g *Group) WriteLine(line string) error {
_, err := g.headBuf.Write([]byte(line + "\n"))
2016-10-28 13:56:31 -07:00
return err
}
// NOTE: g.Head must be closed separately
2016-10-26 16:23:19 -07:00
func (g *Group) Close() error {
g.ticker.Stop()
return nil
}
func (g *Group) processTicks() {
for {
_, ok := <-g.ticker.C
if !ok {
return // Done.
}
2016-10-26 21:50:07 -07:00
g.checkHeadSizeLimit()
g.checkTotalSizeLimit()
2016-10-26 16:23:19 -07:00
}
}
// NOTE: for testing
func (g *Group) stopTicker() {
g.ticker.Stop()
}
// NOTE: this function is called manually in tests.
func (g *Group) checkHeadSizeLimit() {
size, err := g.Head.Size()
if err != nil {
panic(err)
}
if size >= g.HeadSizeLimit() {
g.RotateFile()
}
}
func (g *Group) checkTotalSizeLimit() {
// TODO enforce total size limit
2016-10-28 09:10:33 -07:00
// CHALLENGE
2016-10-26 16:23:19 -07:00
}
func (g *Group) RotateFile() {
g.mtx.Lock()
defer g.mtx.Unlock()
2016-10-26 21:50:07 -07:00
dstPath := filePathForIndex(g.Head.Path, g.maxIndex)
2016-10-26 16:23:19 -07:00
err := os.Rename(g.Head.Path, dstPath)
if err != nil {
panic(err)
}
err = g.Head.closeFile()
if err != nil {
panic(err)
}
2016-10-26 21:50:07 -07:00
g.maxIndex += 1
2016-10-26 16:23:19 -07:00
}
2016-10-30 02:40:39 -07:00
// NOTE: if error, returns no GroupReader.
// CONTRACT: Caller must close the returned GroupReader
func (g *Group) NewReader(index int) (*GroupReader, error) {
2016-10-26 16:23:19 -07:00
r := newGroupReader(g)
2016-10-30 02:40:39 -07:00
err := r.SetIndex(index)
if err != nil {
return nil, err
} else {
return r, nil
}
2016-10-26 16:23:19 -07:00
}
// Returns -1 if line comes after, 0 if found, 1 if line comes before.
type SearchFunc func(line string) (int, error)
2016-11-05 17:58:50 -07:00
// Searches for the right file in Group, then returns a GroupReader to start
// streaming lines.
// Returns true if an exact match was found, otherwise returns the next greater
// line that starts with prefix.
2016-10-30 02:40:39 -07:00
// CONTRACT: Caller must close the returned GroupReader
2016-10-26 21:50:07 -07:00
func (g *Group) Search(prefix string, cmp SearchFunc) (*GroupReader, bool, error) {
g.mtx.Lock()
minIndex, maxIndex := g.minIndex, g.maxIndex
g.mtx.Unlock()
// Now minIndex/maxIndex may change meanwhile,
// but it shouldn't be a big deal
// (maybe we'll want to limit scanUntil though)
2016-10-26 16:23:19 -07:00
for {
2016-10-26 21:50:07 -07:00
curIndex := (minIndex + maxIndex + 1) / 2
2016-10-26 16:23:19 -07:00
// Base case, when there's only 1 choice left.
if minIndex == maxIndex {
2016-10-30 02:40:39 -07:00
r, err := g.NewReader(maxIndex)
if err != nil {
return nil, false, err
}
2016-10-26 21:50:07 -07:00
match, err := scanUntil(r, prefix, cmp)
2016-10-26 16:23:19 -07:00
if err != nil {
r.Close()
2016-10-26 21:50:07 -07:00
return nil, false, err
2016-10-26 16:23:19 -07:00
} else {
2016-10-26 21:50:07 -07:00
return r, match, err
2016-10-26 16:23:19 -07:00
}
}
// Read starting roughly at the middle file,
// until we find line that has prefix.
2016-10-30 02:40:39 -07:00
r, err := g.NewReader(curIndex)
if err != nil {
return nil, false, err
}
foundIndex, line, err := scanNext(r, prefix)
2016-10-26 16:23:19 -07:00
r.Close()
if err != nil {
2016-10-26 21:50:07 -07:00
return nil, false, err
2016-10-26 16:23:19 -07:00
}
// Compare this line to our search query.
val, err := cmp(line)
if err != nil {
2016-10-26 21:50:07 -07:00
return nil, false, err
2016-10-26 16:23:19 -07:00
}
if val < 0 {
// Line will come later
minIndex = foundIndex
} else if val == 0 {
// Stroke of luck, found the line
2016-10-30 02:40:39 -07:00
r, err := g.NewReader(foundIndex)
if err != nil {
return nil, false, err
}
2016-10-26 21:50:07 -07:00
match, err := scanUntil(r, prefix, cmp)
if !match {
panic("Expected match to be true")
}
2016-10-26 16:23:19 -07:00
if err != nil {
r.Close()
2016-10-26 21:50:07 -07:00
return nil, false, err
2016-10-26 16:23:19 -07:00
} else {
2016-10-26 21:50:07 -07:00
return r, true, err
2016-10-26 16:23:19 -07:00
}
} else {
// We passed it
maxIndex = curIndex - 1
}
}
}
// Scans and returns the first line that starts with 'prefix'
2016-10-30 02:40:39 -07:00
// Consumes line and returns it.
func scanNext(r *GroupReader, prefix string) (int, string, error) {
2016-10-26 16:23:19 -07:00
for {
line, err := r.ReadLine()
if err != nil {
return 0, "", err
}
if !strings.HasPrefix(line, prefix) {
continue
}
index := r.CurIndex()
return index, line, nil
}
}
2016-10-26 21:50:07 -07:00
// Returns true iff an exact match was found.
2016-10-30 02:40:39 -07:00
// Pushes line, does not consume it.
2016-10-26 21:50:07 -07:00
func scanUntil(r *GroupReader, prefix string, cmp SearchFunc) (bool, error) {
2016-10-26 16:23:19 -07:00
for {
line, err := r.ReadLine()
if err != nil {
2016-10-26 21:50:07 -07:00
return false, err
2016-10-26 16:23:19 -07:00
}
if !strings.HasPrefix(line, prefix) {
continue
}
val, err := cmp(line)
if err != nil {
2016-10-26 21:50:07 -07:00
return false, err
2016-10-26 16:23:19 -07:00
}
if val < 0 {
continue
2016-10-26 21:50:07 -07:00
} else if val == 0 {
r.PushLine(line)
return true, nil
2016-10-26 16:23:19 -07:00
} else {
r.PushLine(line)
2016-10-26 21:50:07 -07:00
return false, nil
2016-10-26 16:23:19 -07:00
}
}
}
2016-11-05 17:58:50 -07:00
// Searches backwards for the last line in Group with prefix.
// Scans each file forward until the end to find the last match.
2016-10-30 02:40:39 -07:00
func (g *Group) FindLast(prefix string) (match string, found bool, err error) {
g.mtx.Lock()
minIndex, maxIndex := g.minIndex, g.maxIndex
g.mtx.Unlock()
r, err := g.NewReader(maxIndex)
if err != nil {
return "", false, err
}
defer r.Close()
// Open files from the back and read
GROUP_LOOP:
for i := maxIndex; i >= minIndex; i-- {
err := r.SetIndex(i)
if err != nil {
return "", false, err
}
// Scan each line and test whether line matches
for {
line, err := r.ReadLine()
2016-10-30 02:40:39 -07:00
if err == io.EOF {
if found {
return match, found, nil
} else {
continue GROUP_LOOP
}
} else if err != nil {
return "", false, err
}
if strings.HasPrefix(line, prefix) {
match = line
found = true
}
if r.CurIndex() > i {
if found {
return match, found, nil
} else {
continue GROUP_LOOP
}
}
2016-10-30 02:40:39 -07:00
}
}
return
}
2016-10-26 16:23:19 -07:00
type GroupInfo struct {
MinIndex int
MaxIndex int
TotalSize int64
HeadSize int64
}
// Returns info after scanning all files in g.Head's dir
func (g *Group) ReadGroupInfo() GroupInfo {
g.mtx.Lock()
defer g.mtx.Unlock()
return g.readGroupInfo()
}
2016-10-26 21:50:07 -07:00
// Index includes the head.
2016-10-26 16:23:19 -07:00
// CONTRACT: caller should have called g.mtx.Lock
func (g *Group) readGroupInfo() GroupInfo {
groupDir := filepath.Dir(g.Head.Path)
headBase := filepath.Base(g.Head.Path)
var minIndex, maxIndex int = -1, -1
var totalSize, headSize int64 = 0, 0
dir, err := os.Open(groupDir)
if err != nil {
panic(err)
}
fiz, err := dir.Readdir(0)
if err != nil {
panic(err)
}
// For each file in the directory, filter by pattern
for _, fileInfo := range fiz {
if fileInfo.Name() == headBase {
fileSize := fileInfo.Size()
totalSize += fileSize
headSize = fileSize
continue
} else if strings.HasPrefix(fileInfo.Name(), headBase) {
fileSize := fileInfo.Size()
totalSize += fileSize
indexedFilePattern := regexp.MustCompile(`^.+\.([0-9]{3,})$`)
submatch := indexedFilePattern.FindSubmatch([]byte(fileInfo.Name()))
if len(submatch) != 0 {
// Matches
fileIndex, err := strconv.Atoi(string(submatch[1]))
if err != nil {
panic(err)
}
if maxIndex < fileIndex {
maxIndex = fileIndex
}
if minIndex == -1 || fileIndex < minIndex {
minIndex = fileIndex
}
}
}
}
2016-10-26 21:50:07 -07:00
// Now account for the head.
if minIndex == -1 {
// If there were no numbered files,
// then the head is index 0.
minIndex, maxIndex = 0, 0
} else {
// Otherwise, the head file is 1 greater
maxIndex += 1
}
2016-10-26 16:23:19 -07:00
return GroupInfo{minIndex, maxIndex, totalSize, headSize}
}
func filePathForIndex(headPath string, index int) string {
return fmt.Sprintf("%v.%03d", headPath, index)
}
//--------------------------------------------------------------------------------
type GroupReader struct {
*Group
mtx sync.Mutex
curIndex int
curFile *os.File
curReader *bufio.Reader
curLine []byte
}
func newGroupReader(g *Group) *GroupReader {
return &GroupReader{
Group: g,
2016-10-26 21:50:07 -07:00
curIndex: 0,
2016-10-26 16:23:19 -07:00
curFile: nil,
curReader: nil,
curLine: nil,
}
}
2016-10-26 21:50:07 -07:00
func (gr *GroupReader) Close() error {
gr.mtx.Lock()
defer gr.mtx.Unlock()
if gr.curReader != nil {
err := gr.curFile.Close()
gr.curIndex = 0
gr.curReader = nil
gr.curFile = nil
gr.curLine = nil
return err
} else {
return nil
}
}
// Reads a line (without delimiter)
// just return io.EOF if no new lines found.
2016-10-26 21:50:07 -07:00
func (gr *GroupReader) ReadLine() (string, error) {
gr.mtx.Lock()
defer gr.mtx.Unlock()
2016-10-26 16:23:19 -07:00
// From PushLine
2016-10-26 21:50:07 -07:00
if gr.curLine != nil {
line := string(gr.curLine)
gr.curLine = nil
2016-10-26 16:23:19 -07:00
return line, nil
}
// Open file if not open yet
2016-10-26 21:50:07 -07:00
if gr.curReader == nil {
err := gr.openFile(gr.curIndex)
2016-10-26 16:23:19 -07:00
if err != nil {
return "", err
}
}
// Iterate over files until line is found
var linePrefix string
2016-10-26 16:23:19 -07:00
for {
bytesRead, err := gr.curReader.ReadBytes('\n')
if err == io.EOF {
// Open the next file
err := gr.openFile(gr.curIndex + 1)
if err != nil {
2016-10-30 02:40:39 -07:00
return "", err
}
if len(bytesRead) > 0 && bytesRead[len(bytesRead)-1] == byte('\n') {
return linePrefix + string(bytesRead[:len(bytesRead)-1]), nil
2016-10-26 16:23:19 -07:00
} else {
linePrefix += string(bytesRead)
2016-10-26 21:50:07 -07:00
continue
2016-10-26 16:23:19 -07:00
}
} else if err != nil {
return "", err
2016-10-26 16:23:19 -07:00
}
return linePrefix + string(bytesRead[:len(bytesRead)-1]), nil
2016-10-26 16:23:19 -07:00
}
}
2016-10-26 21:50:07 -07:00
// IF index > gr.Group.maxIndex, returns io.EOF
// CONTRACT: caller should hold gr.mtx
func (gr *GroupReader) openFile(index int) error {
2016-10-26 16:23:19 -07:00
// Lock on Group to ensure that head doesn't move in the meanwhile.
2016-10-26 21:50:07 -07:00
gr.Group.mtx.Lock()
defer gr.Group.mtx.Unlock()
var curFilePath string
if index == gr.Group.maxIndex {
curFilePath = gr.Head.Path
} else if index > gr.Group.maxIndex {
return io.EOF
} else {
curFilePath = filePathForIndex(gr.Head.Path, index)
}
2016-10-26 16:23:19 -07:00
curFile, err := os.Open(curFilePath)
if err != nil {
return err
}
curReader := bufio.NewReader(curFile)
2016-10-26 21:50:07 -07:00
// Update gr.cur*
gr.curIndex = index
gr.curFile = curFile
gr.curReader = curReader
gr.curLine = nil
2016-10-26 16:23:19 -07:00
return nil
}
2016-10-26 21:50:07 -07:00
func (gr *GroupReader) PushLine(line string) {
gr.mtx.Lock()
defer gr.mtx.Unlock()
2016-10-26 16:23:19 -07:00
2016-10-26 21:50:07 -07:00
if gr.curLine == nil {
gr.curLine = []byte(line)
2016-10-26 16:23:19 -07:00
} else {
panic("PushLine failed, already have line")
}
}
// Cursor's file index.
2016-10-26 21:50:07 -07:00
func (gr *GroupReader) CurIndex() int {
gr.mtx.Lock()
defer gr.mtx.Unlock()
return gr.curIndex
2016-10-26 16:23:19 -07:00
}
2016-10-30 02:40:39 -07:00
func (gr *GroupReader) SetIndex(index int) error {
2016-10-26 21:50:07 -07:00
gr.mtx.Lock()
defer gr.mtx.Unlock()
2016-10-30 02:40:39 -07:00
return gr.openFile(index)
2016-10-26 16:23:19 -07:00
}
2016-11-05 17:58:50 -07:00
//--------------------------------------------------------------------------------
// A simple SearchFunc that assumes that the marker is of form
// <prefix><number>.
// For example, if prefix is '#HEIGHT:', the markers of expected to be of the form:
//
// #HEIGHT:1
// ...
// #HEIGHT:2
// ...
func MakeSimpleSearchFunc(prefix string, target int) SearchFunc {
return func(line string) (int, error) {
if !strings.HasPrefix(line, prefix) {
return -1, errors.New(Fmt("Marker line did not have prefix: %v", prefix))
}
i, err := strconv.Atoi(line[len(prefix):])
if err != nil {
return -1, errors.New(Fmt("Failed to parse marker line: %v", err.Error()))
}
if target < i {
return 1, nil
} else if target == i {
return 0, nil
} else {
return -1, nil
}
}
}