github.com/supragya/TendermintConnector@v0.0.0-20210619045051-113e32b84fb1/_deprecated_chains/irisnet/libs/autofile/group.go (about)

     1  package autofile
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	cmn "github.com/tendermint/tendermint/libs/common"
    18  )
    19  
    20  const (
    21  	defaultGroupCheckDuration = 5000 * time.Millisecond
    22  	defaultHeadSizeLimit      = 10 * 1024 * 1024       // 10MB
    23  	defaultTotalSizeLimit     = 1 * 1024 * 1024 * 1024 // 1GB
    24  	maxFilesToRemove          = 4                      // needs to be greater than 1
    25  )
    26  
    27  /*
    28  You can open a Group to keep restrictions on an AutoFile, like
    29  the maximum size of each chunk, and/or the total amount of bytes
    30  stored in the group.
    31  
    32  The first file to be written in the Group.Dir is the head file.
    33  
    34  	Dir/
    35  	- <HeadPath>
    36  
    37  Once the Head file reaches the size limit, it will be rotated.
    38  
    39  	Dir/
    40  	- <HeadPath>.000   // First rolled file
    41  	- <HeadPath>       // New head path, starts empty.
    42  										 // The implicit index is 001.
    43  
    44  As more files are written, the index numbers grow...
    45  
    46  	Dir/
    47  	- <HeadPath>.000   // First rolled file
    48  	- <HeadPath>.001   // Second rolled file
    49  	- ...
    50  	- <HeadPath>       // New head path
    51  
    52  The Group can also be used to binary-search for some line,
    53  assuming that marker lines are written occasionally.
    54  */
    55  type Group struct {
    56  	cmn.BaseService
    57  
    58  	ID                 string
    59  	Head               *AutoFile // The head AutoFile to write to
    60  	headBuf            *bufio.Writer
    61  	Dir                string // Directory that contains .Head
    62  	ticker             *time.Ticker
    63  	mtx                sync.Mutex
    64  	headSizeLimit      int64
    65  	totalSizeLimit     int64
    66  	groupCheckDuration time.Duration
    67  	minIndex           int // Includes head
    68  	maxIndex           int // Includes head, where Head will move to
    69  
    70  	// close this when the processTicks routine is done.
    71  	// this ensures we can cleanup the dir after calling Stop
    72  	// and the routine won't be trying to access it anymore
    73  	doneProcessTicks chan struct{}
    74  }
    75  
    76  // OpenGroup creates a new Group with head at headPath. It returns an error if
    77  // it fails to open head file.
    78  func OpenGroup(headPath string, groupOptions ...func(*Group)) (g *Group, err error) {
    79  	dir := path.Dir(headPath)
    80  	head, err := OpenAutoFile(headPath)
    81  	if err != nil {
    82  		return nil, err
    83  	}
    84  
    85  	g = &Group{
    86  		ID:                 "group:" + head.ID,
    87  		Head:               head,
    88  		headBuf:            bufio.NewWriterSize(head, 4096*10),
    89  		Dir:                dir,
    90  		headSizeLimit:      defaultHeadSizeLimit,
    91  		totalSizeLimit:     defaultTotalSizeLimit,
    92  		groupCheckDuration: defaultGroupCheckDuration,
    93  		minIndex:           0,
    94  		maxIndex:           0,
    95  		doneProcessTicks:   make(chan struct{}),
    96  	}
    97  
    98  	for _, option := range groupOptions {
    99  		option(g)
   100  	}
   101  
   102  	g.BaseService = *cmn.NewBaseService(nil, "Group", g)
   103  
   104  	gInfo := g.readGroupInfo()
   105  	g.minIndex = gInfo.MinIndex
   106  	g.maxIndex = gInfo.MaxIndex
   107  	return
   108  }
   109  
   110  // GroupCheckDuration allows you to overwrite default groupCheckDuration.
   111  func GroupCheckDuration(duration time.Duration) func(*Group) {
   112  	return func(g *Group) {
   113  		g.groupCheckDuration = duration
   114  	}
   115  }
   116  
   117  // GroupHeadSizeLimit allows you to overwrite default head size limit - 10MB.
   118  func GroupHeadSizeLimit(limit int64) func(*Group) {
   119  	return func(g *Group) {
   120  		g.headSizeLimit = limit
   121  	}
   122  }
   123  
   124  // GroupTotalSizeLimit allows you to overwrite default total size limit of the group - 1GB.
   125  func GroupTotalSizeLimit(limit int64) func(*Group) {
   126  	return func(g *Group) {
   127  		g.totalSizeLimit = limit
   128  	}
   129  }
   130  
   131  // OnStart implements Service by starting the goroutine that checks file and
   132  // group limits.
   133  func (g *Group) OnStart() error {
   134  	g.ticker = time.NewTicker(g.groupCheckDuration)
   135  	go g.processTicks()
   136  	return nil
   137  }
   138  
   139  // OnStop implements Service by stopping the goroutine described above.
   140  // NOTE: g.Head must be closed separately using Close.
   141  func (g *Group) OnStop() {
   142  	g.ticker.Stop()
   143  	g.Flush() // flush any uncommitted data
   144  }
   145  
   146  func (g *Group) Wait() {
   147  	// wait for processTicks routine to finish
   148  	<-g.doneProcessTicks
   149  }
   150  
   151  // Close closes the head file. The group must be stopped by this moment.
   152  func (g *Group) Close() {
   153  	g.Flush() // flush any uncommitted data
   154  
   155  	g.mtx.Lock()
   156  	_ = g.Head.closeFile()
   157  	g.mtx.Unlock()
   158  }
   159  
   160  // HeadSizeLimit returns the current head size limit.
   161  func (g *Group) HeadSizeLimit() int64 {
   162  	g.mtx.Lock()
   163  	defer g.mtx.Unlock()
   164  	return g.headSizeLimit
   165  }
   166  
   167  // TotalSizeLimit returns total size limit of the group.
   168  func (g *Group) TotalSizeLimit() int64 {
   169  	g.mtx.Lock()
   170  	defer g.mtx.Unlock()
   171  	return g.totalSizeLimit
   172  }
   173  
   174  // MaxIndex returns index of the last file in the group.
   175  func (g *Group) MaxIndex() int {
   176  	g.mtx.Lock()
   177  	defer g.mtx.Unlock()
   178  	return g.maxIndex
   179  }
   180  
   181  // MinIndex returns index of the first file in the group.
   182  func (g *Group) MinIndex() int {
   183  	g.mtx.Lock()
   184  	defer g.mtx.Unlock()
   185  	return g.minIndex
   186  }
   187  
   188  func (g *Group) Write(p []byte) (nn int, err error) {
   189  	g.mtx.Lock()
   190  	defer g.mtx.Unlock()
   191  	return g.headBuf.Write(p)
   192  }
   193  
   194  func (g *Group) WriteLine(line string) error {
   195  	g.mtx.Lock()
   196  	defer g.mtx.Unlock()
   197  	_, err := g.headBuf.Write([]byte(line + "\n"))
   198  	return err
   199  }
   200  
   201  // Flush writes any buffered data to the underlying file and commits the
   202  // current content of the file to stable storage.
   203  func (g *Group) Flush() error {
   204  	g.mtx.Lock()
   205  	defer g.mtx.Unlock()
   206  	err := g.headBuf.Flush()
   207  	if err == nil {
   208  		err = g.Head.Sync()
   209  	}
   210  	return err
   211  }
   212  
   213  func (g *Group) processTicks() {
   214  	defer close(g.doneProcessTicks)
   215  	for {
   216  		select {
   217  		case <-g.ticker.C:
   218  			g.checkHeadSizeLimit()
   219  			g.checkTotalSizeLimit()
   220  		case <-g.Quit():
   221  			return
   222  		}
   223  	}
   224  }
   225  
   226  // NOTE: this function is called manually in tests.
   227  func (g *Group) checkHeadSizeLimit() {
   228  	limit := g.HeadSizeLimit()
   229  	if limit == 0 {
   230  		return
   231  	}
   232  	size, err := g.Head.Size()
   233  	if err != nil {
   234  		g.Logger.Error("Group's head may grow without bound", "head", g.Head.Path, "err", err)
   235  		return
   236  	}
   237  	if size >= limit {
   238  		g.RotateFile()
   239  	}
   240  }
   241  
   242  func (g *Group) checkTotalSizeLimit() {
   243  	limit := g.TotalSizeLimit()
   244  	if limit == 0 {
   245  		return
   246  	}
   247  
   248  	gInfo := g.readGroupInfo()
   249  	totalSize := gInfo.TotalSize
   250  	for i := 0; i < maxFilesToRemove; i++ {
   251  		index := gInfo.MinIndex + i
   252  		if totalSize < limit {
   253  			return
   254  		}
   255  		if index == gInfo.MaxIndex {
   256  			// Special degenerate case, just do nothing.
   257  			g.Logger.Error("Group's head may grow without bound", "head", g.Head.Path)
   258  			return
   259  		}
   260  		pathToRemove := filePathForIndex(g.Head.Path, index, gInfo.MaxIndex)
   261  		fInfo, err := os.Stat(pathToRemove)
   262  		if err != nil {
   263  			g.Logger.Error("Failed to fetch info for file", "file", pathToRemove)
   264  			continue
   265  		}
   266  		err = os.Remove(pathToRemove)
   267  		if err != nil {
   268  			g.Logger.Error("Failed to remove path", "path", pathToRemove)
   269  			return
   270  		}
   271  		totalSize -= fInfo.Size()
   272  	}
   273  }
   274  
   275  // RotateFile causes group to close the current head and assign it some index.
   276  // Note it does not create a new head.
   277  func (g *Group) RotateFile() {
   278  	g.mtx.Lock()
   279  	defer g.mtx.Unlock()
   280  
   281  	headPath := g.Head.Path
   282  
   283  	if err := g.headBuf.Flush(); err != nil {
   284  		panic(err)
   285  	}
   286  
   287  	if err := g.Head.Sync(); err != nil {
   288  		panic(err)
   289  	}
   290  
   291  	if err := g.Head.closeFile(); err != nil {
   292  		panic(err)
   293  	}
   294  
   295  	indexPath := filePathForIndex(headPath, g.maxIndex, g.maxIndex+1)
   296  	if err := os.Rename(headPath, indexPath); err != nil {
   297  		panic(err)
   298  	}
   299  
   300  	g.maxIndex++
   301  }
   302  
   303  // NewReader returns a new group reader.
   304  // CONTRACT: Caller must close the returned GroupReader.
   305  func (g *Group) NewReader(index int) (*GroupReader, error) {
   306  	r := newGroupReader(g)
   307  	err := r.SetIndex(index)
   308  	if err != nil {
   309  		return nil, err
   310  	}
   311  	return r, nil
   312  }
   313  
   314  // Returns -1 if line comes after, 0 if found, 1 if line comes before.
   315  type SearchFunc func(line string) (int, error)
   316  
   317  // Searches for the right file in Group, then returns a GroupReader to start
   318  // streaming lines.
   319  // Returns true if an exact match was found, otherwise returns the next greater
   320  // line that starts with prefix.
   321  // CONTRACT: Caller must close the returned GroupReader
   322  func (g *Group) Search(prefix string, cmp SearchFunc) (*GroupReader, bool, error) {
   323  	g.mtx.Lock()
   324  	minIndex, maxIndex := g.minIndex, g.maxIndex
   325  	g.mtx.Unlock()
   326  	// Now minIndex/maxIndex may change meanwhile,
   327  	// but it shouldn't be a big deal
   328  	// (maybe we'll want to limit scanUntil though)
   329  
   330  	for {
   331  		curIndex := (minIndex + maxIndex + 1) / 2
   332  
   333  		// Base case, when there's only 1 choice left.
   334  		if minIndex == maxIndex {
   335  			r, err := g.NewReader(maxIndex)
   336  			if err != nil {
   337  				return nil, false, err
   338  			}
   339  			match, err := scanUntil(r, prefix, cmp)
   340  			if err != nil {
   341  				r.Close()
   342  				return nil, false, err
   343  			}
   344  			return r, match, err
   345  		}
   346  
   347  		// Read starting roughly at the middle file,
   348  		// until we find line that has prefix.
   349  		r, err := g.NewReader(curIndex)
   350  		if err != nil {
   351  			return nil, false, err
   352  		}
   353  		foundIndex, line, err := scanNext(r, prefix)
   354  		r.Close()
   355  		if err != nil {
   356  			return nil, false, err
   357  		}
   358  
   359  		// Compare this line to our search query.
   360  		val, err := cmp(line)
   361  		if err != nil {
   362  			return nil, false, err
   363  		}
   364  		if val < 0 {
   365  			// Line will come later
   366  			minIndex = foundIndex
   367  		} else if val == 0 {
   368  			// Stroke of luck, found the line
   369  			r, err := g.NewReader(foundIndex)
   370  			if err != nil {
   371  				return nil, false, err
   372  			}
   373  			match, err := scanUntil(r, prefix, cmp)
   374  			if !match {
   375  				panic("Expected match to be true")
   376  			}
   377  			if err != nil {
   378  				r.Close()
   379  				return nil, false, err
   380  			}
   381  			return r, true, err
   382  		} else {
   383  			// We passed it
   384  			maxIndex = curIndex - 1
   385  		}
   386  	}
   387  
   388  }
   389  
   390  // Scans and returns the first line that starts with 'prefix'
   391  // Consumes line and returns it.
   392  func scanNext(r *GroupReader, prefix string) (int, string, error) {
   393  	for {
   394  		line, err := r.ReadLine()
   395  		if err != nil {
   396  			return 0, "", err
   397  		}
   398  		if !strings.HasPrefix(line, prefix) {
   399  			continue
   400  		}
   401  		index := r.CurIndex()
   402  		return index, line, nil
   403  	}
   404  }
   405  
   406  // Returns true iff an exact match was found.
   407  // Pushes line, does not consume it.
   408  func scanUntil(r *GroupReader, prefix string, cmp SearchFunc) (bool, error) {
   409  	for {
   410  		line, err := r.ReadLine()
   411  		if err != nil {
   412  			return false, err
   413  		}
   414  		if !strings.HasPrefix(line, prefix) {
   415  			continue
   416  		}
   417  		val, err := cmp(line)
   418  		if err != nil {
   419  			return false, err
   420  		}
   421  		if val < 0 {
   422  			continue
   423  		} else if val == 0 {
   424  			r.PushLine(line)
   425  			return true, nil
   426  		} else {
   427  			r.PushLine(line)
   428  			return false, nil
   429  		}
   430  	}
   431  }
   432  
   433  // Searches backwards for the last line in Group with prefix.
   434  // Scans each file forward until the end to find the last match.
   435  func (g *Group) FindLast(prefix string) (match string, found bool, err error) {
   436  	g.mtx.Lock()
   437  	minIndex, maxIndex := g.minIndex, g.maxIndex
   438  	g.mtx.Unlock()
   439  
   440  	r, err := g.NewReader(maxIndex)
   441  	if err != nil {
   442  		return "", false, err
   443  	}
   444  	defer r.Close()
   445  
   446  	// Open files from the back and read
   447  GROUP_LOOP:
   448  	for i := maxIndex; i >= minIndex; i-- {
   449  		err := r.SetIndex(i)
   450  		if err != nil {
   451  			return "", false, err
   452  		}
   453  		// Scan each line and test whether line matches
   454  		for {
   455  			line, err := r.ReadLine()
   456  			if err == io.EOF {
   457  				if found {
   458  					return match, found, nil
   459  				}
   460  				continue GROUP_LOOP
   461  			} else if err != nil {
   462  				return "", false, err
   463  			}
   464  			if strings.HasPrefix(line, prefix) {
   465  				match = line
   466  				found = true
   467  			}
   468  			if r.CurIndex() > i {
   469  				if found {
   470  					return match, found, nil
   471  				}
   472  				continue GROUP_LOOP
   473  			}
   474  		}
   475  	}
   476  
   477  	return
   478  }
   479  
   480  // GroupInfo holds information about the group.
   481  type GroupInfo struct {
   482  	MinIndex  int   // index of the first file in the group, including head
   483  	MaxIndex  int   // index of the last file in the group, including head
   484  	TotalSize int64 // total size of the group
   485  	HeadSize  int64 // size of the head
   486  }
   487  
   488  // Returns info after scanning all files in g.Head's dir.
   489  func (g *Group) ReadGroupInfo() GroupInfo {
   490  	g.mtx.Lock()
   491  	defer g.mtx.Unlock()
   492  	return g.readGroupInfo()
   493  }
   494  
   495  // Index includes the head.
   496  // CONTRACT: caller should have called g.mtx.Lock
   497  func (g *Group) readGroupInfo() GroupInfo {
   498  	groupDir := filepath.Dir(g.Head.Path)
   499  	headBase := filepath.Base(g.Head.Path)
   500  	var minIndex, maxIndex int = -1, -1
   501  	var totalSize, headSize int64 = 0, 0
   502  
   503  	dir, err := os.Open(groupDir)
   504  	if err != nil {
   505  		panic(err)
   506  	}
   507  	defer dir.Close()
   508  	fiz, err := dir.Readdir(0)
   509  	if err != nil {
   510  		panic(err)
   511  	}
   512  
   513  	// For each file in the directory, filter by pattern
   514  	for _, fileInfo := range fiz {
   515  		if fileInfo.Name() == headBase {
   516  			fileSize := fileInfo.Size()
   517  			totalSize += fileSize
   518  			headSize = fileSize
   519  			continue
   520  		} else if strings.HasPrefix(fileInfo.Name(), headBase) {
   521  			fileSize := fileInfo.Size()
   522  			totalSize += fileSize
   523  			indexedFilePattern := regexp.MustCompile(`^.+\.([0-9]{3,})$`)
   524  			submatch := indexedFilePattern.FindSubmatch([]byte(fileInfo.Name()))
   525  			if len(submatch) != 0 {
   526  				// Matches
   527  				fileIndex, err := strconv.Atoi(string(submatch[1]))
   528  				if err != nil {
   529  					panic(err)
   530  				}
   531  				if maxIndex < fileIndex {
   532  					maxIndex = fileIndex
   533  				}
   534  				if minIndex == -1 || fileIndex < minIndex {
   535  					minIndex = fileIndex
   536  				}
   537  			}
   538  		}
   539  	}
   540  
   541  	// Now account for the head.
   542  	if minIndex == -1 {
   543  		// If there were no numbered files,
   544  		// then the head is index 0.
   545  		minIndex, maxIndex = 0, 0
   546  	} else {
   547  		// Otherwise, the head file is 1 greater
   548  		maxIndex++
   549  	}
   550  	return GroupInfo{minIndex, maxIndex, totalSize, headSize}
   551  }
   552  
   553  func filePathForIndex(headPath string, index int, maxIndex int) string {
   554  	if index == maxIndex {
   555  		return headPath
   556  	}
   557  	return fmt.Sprintf("%v.%03d", headPath, index)
   558  }
   559  
   560  //--------------------------------------------------------------------------------
   561  
   562  // GroupReader provides an interface for reading from a Group.
   563  type GroupReader struct {
   564  	*Group
   565  	mtx       sync.Mutex
   566  	curIndex  int
   567  	curFile   *os.File
   568  	curReader *bufio.Reader
   569  	curLine   []byte
   570  }
   571  
   572  func newGroupReader(g *Group) *GroupReader {
   573  	return &GroupReader{
   574  		Group:     g,
   575  		curIndex:  0,
   576  		curFile:   nil,
   577  		curReader: nil,
   578  		curLine:   nil,
   579  	}
   580  }
   581  
   582  // Close closes the GroupReader by closing the cursor file.
   583  func (gr *GroupReader) Close() error {
   584  	gr.mtx.Lock()
   585  	defer gr.mtx.Unlock()
   586  
   587  	if gr.curReader != nil {
   588  		err := gr.curFile.Close()
   589  		gr.curIndex = 0
   590  		gr.curReader = nil
   591  		gr.curFile = nil
   592  		gr.curLine = nil
   593  		return err
   594  	}
   595  	return nil
   596  }
   597  
   598  // Read implements io.Reader, reading bytes from the current Reader
   599  // incrementing index until enough bytes are read.
   600  func (gr *GroupReader) Read(p []byte) (n int, err error) {
   601  	lenP := len(p)
   602  	if lenP == 0 {
   603  		return 0, errors.New("given empty slice")
   604  	}
   605  
   606  	gr.mtx.Lock()
   607  	defer gr.mtx.Unlock()
   608  
   609  	// Open file if not open yet
   610  	if gr.curReader == nil {
   611  		if err = gr.openFile(gr.curIndex); err != nil {
   612  			return 0, err
   613  		}
   614  	}
   615  
   616  	// Iterate over files until enough bytes are read
   617  	var nn int
   618  	for {
   619  		nn, err = gr.curReader.Read(p[n:])
   620  		n += nn
   621  		if err == io.EOF {
   622  			if n >= lenP {
   623  				return n, nil
   624  			}
   625  			// Open the next file
   626  			if err1 := gr.openFile(gr.curIndex + 1); err1 != nil {
   627  				return n, err1
   628  			}
   629  		} else if err != nil {
   630  			return n, err
   631  		} else if nn == 0 { // empty file
   632  			return n, err
   633  		}
   634  	}
   635  }
   636  
   637  // ReadLine reads a line (without delimiter).
   638  // just return io.EOF if no new lines found.
   639  func (gr *GroupReader) ReadLine() (string, error) {
   640  	gr.mtx.Lock()
   641  	defer gr.mtx.Unlock()
   642  
   643  	// From PushLine
   644  	if gr.curLine != nil {
   645  		line := string(gr.curLine)
   646  		gr.curLine = nil
   647  		return line, nil
   648  	}
   649  
   650  	// Open file if not open yet
   651  	if gr.curReader == nil {
   652  		err := gr.openFile(gr.curIndex)
   653  		if err != nil {
   654  			return "", err
   655  		}
   656  	}
   657  
   658  	// Iterate over files until line is found
   659  	var linePrefix string
   660  	for {
   661  		bytesRead, err := gr.curReader.ReadBytes('\n')
   662  		if err == io.EOF {
   663  			// Open the next file
   664  			if err1 := gr.openFile(gr.curIndex + 1); err1 != nil {
   665  				return "", err1
   666  			}
   667  			if len(bytesRead) > 0 && bytesRead[len(bytesRead)-1] == byte('\n') {
   668  				return linePrefix + string(bytesRead[:len(bytesRead)-1]), nil
   669  			}
   670  			linePrefix += string(bytesRead)
   671  			continue
   672  		} else if err != nil {
   673  			return "", err
   674  		}
   675  		return linePrefix + string(bytesRead[:len(bytesRead)-1]), nil
   676  	}
   677  }
   678  
   679  // IF index > gr.Group.maxIndex, returns io.EOF
   680  // CONTRACT: caller should hold gr.mtx
   681  func (gr *GroupReader) openFile(index int) error {
   682  	// Lock on Group to ensure that head doesn't move in the meanwhile.
   683  	gr.Group.mtx.Lock()
   684  	defer gr.Group.mtx.Unlock()
   685  
   686  	if index > gr.Group.maxIndex {
   687  		return io.EOF
   688  	}
   689  
   690  	curFilePath := filePathForIndex(gr.Head.Path, index, gr.Group.maxIndex)
   691  	curFile, err := os.OpenFile(curFilePath, os.O_RDONLY|os.O_CREATE, autoFilePerms)
   692  	if err != nil {
   693  		return err
   694  	}
   695  	curReader := bufio.NewReader(curFile)
   696  
   697  	// Update gr.cur*
   698  	if gr.curFile != nil {
   699  		gr.curFile.Close()
   700  	}
   701  	gr.curIndex = index
   702  	gr.curFile = curFile
   703  	gr.curReader = curReader
   704  	gr.curLine = nil
   705  	return nil
   706  }
   707  
   708  // PushLine makes the given line the current one, so the next time somebody
   709  // calls ReadLine, this line will be returned.
   710  // panics if called twice without calling ReadLine.
   711  func (gr *GroupReader) PushLine(line string) {
   712  	gr.mtx.Lock()
   713  	defer gr.mtx.Unlock()
   714  
   715  	if gr.curLine == nil {
   716  		gr.curLine = []byte(line)
   717  	} else {
   718  		panic("PushLine failed, already have line")
   719  	}
   720  }
   721  
   722  // CurIndex returns cursor's file index.
   723  func (gr *GroupReader) CurIndex() int {
   724  	gr.mtx.Lock()
   725  	defer gr.mtx.Unlock()
   726  	return gr.curIndex
   727  }
   728  
   729  // SetIndex sets the cursor's file index to index by opening a file at this
   730  // position.
   731  func (gr *GroupReader) SetIndex(index int) error {
   732  	gr.mtx.Lock()
   733  	defer gr.mtx.Unlock()
   734  	return gr.openFile(index)
   735  }
   736  
   737  //--------------------------------------------------------------------------------
   738  
   739  // A simple SearchFunc that assumes that the marker is of form
   740  // <prefix><number>.
   741  // For example, if prefix is '#HEIGHT:', the markers of expected to be of the form:
   742  //
   743  // #HEIGHT:1
   744  // ...
   745  // #HEIGHT:2
   746  // ...
   747  func MakeSimpleSearchFunc(prefix string, target int) SearchFunc {
   748  	return func(line string) (int, error) {
   749  		if !strings.HasPrefix(line, prefix) {
   750  			return -1, fmt.Errorf("Marker line did not have prefix: %v", prefix)
   751  		}
   752  		i, err := strconv.Atoi(line[len(prefix):])
   753  		if err != nil {
   754  			return -1, fmt.Errorf("Failed to parse marker line: %v", err.Error())
   755  		}
   756  		if target < i {
   757  			return 1, nil
   758  		} else if target == i {
   759  			return 0, nil
   760  		} else {
   761  			return -1, nil
   762  		}
   763  	}
   764  }