You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

763 lines
18 KiB

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