github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/disk_journal.go (about)

     1  // Copyright 2016 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package libkbfs
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"strconv"
    13  
    14  	"github.com/keybase/client/go/kbfs/ioutil"
    15  	"github.com/keybase/client/go/kbfs/kbfscodec"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // journalOrdinal is the ordinal used for naming journal entries.
    20  type journalOrdinal uint64
    21  
    22  // TODO: Define the zero journalOrdinal as invalid, once no existing
    23  // journals use them.
    24  
    25  const firstValidJournalOrdinal journalOrdinal = 1
    26  
    27  func makeJournalOrdinal(s string) (journalOrdinal, error) {
    28  	if len(s) != 16 {
    29  		return 0, errors.Errorf("invalid journal ordinal %q", s)
    30  	}
    31  	u, err := strconv.ParseUint(s, 16, 64)
    32  	if err != nil {
    33  		return 0, errors.Wrapf(err, "failed to parse %q", s)
    34  	}
    35  	return journalOrdinal(u), nil
    36  }
    37  
    38  func (o journalOrdinal) String() string {
    39  	return fmt.Sprintf("%016x", uint64(o))
    40  }
    41  
    42  // diskJournal stores an ordered list of entries in a directory, which
    43  // is assumed to not be used by anything else.
    44  //
    45  // The directory layout looks like:
    46  //
    47  // dir/EARLIEST
    48  // dir/LATEST
    49  // dir/0...000
    50  // dir/0...001
    51  // dir/0...fff
    52  //
    53  // Each file in dir is named with an ordinal and contains a generic
    54  // serializable entry object. The files EARLIEST and LATEST point to
    55  // the earliest and latest valid ordinal, respectively.
    56  //
    57  // This class is not goroutine-safe; it assumes that all
    58  // synchronization is done at a higher level.
    59  //
    60  // TODO: Do all high-level operations atomically on the file-system
    61  // level.
    62  //
    63  // TODO: Make IO ops cancellable.
    64  type diskJournal struct {
    65  	codec     kbfscodec.Codec
    66  	dir       string
    67  	entryType reflect.Type
    68  
    69  	// The journal must be considered empty when either
    70  	// earliestValid or latestValid is false.
    71  
    72  	earliestValid bool
    73  	earliest      journalOrdinal
    74  
    75  	latestValid bool
    76  	latest      journalOrdinal
    77  }
    78  
    79  // makeDiskJournal returns a new diskJournal for the given directory.
    80  func makeDiskJournal(
    81  	codec kbfscodec.Codec, dir string, entryType reflect.Type) (
    82  	*diskJournal, error) {
    83  	j := &diskJournal{
    84  		codec:     codec,
    85  		dir:       dir,
    86  		entryType: entryType,
    87  	}
    88  
    89  	earliest, err := j.readEarliestOrdinalFromDisk()
    90  	switch {
    91  	case ioutil.IsNotExist(err):
    92  		// Continue with j.earliestValid = false.
    93  	case err != nil:
    94  		return nil, err
    95  	default:
    96  		j.earliestValid = true
    97  		j.earliest = earliest
    98  	}
    99  
   100  	latest, err := j.readLatestOrdinalFromDisk()
   101  	switch {
   102  	case ioutil.IsNotExist(err):
   103  		// Continue with j.latestValid = false.
   104  	case err != nil:
   105  		return nil, err
   106  	default:
   107  		j.latestValid = true
   108  		j.latest = latest
   109  	}
   110  
   111  	return j, nil
   112  }
   113  
   114  // The functions below are for building various paths for the journal.
   115  
   116  func (j diskJournal) earliestPath() string {
   117  	return filepath.Join(j.dir, "EARLIEST")
   118  }
   119  
   120  func (j diskJournal) latestPath() string {
   121  	return filepath.Join(j.dir, "LATEST")
   122  }
   123  
   124  func (j diskJournal) journalEntryPath(o journalOrdinal) string {
   125  	return filepath.Join(j.dir, o.String())
   126  }
   127  
   128  // The functions below are for reading and writing the earliest and
   129  // latest ordinals. The read functions may return an error for which
   130  // ioutil.IsNotExist() returns true.
   131  
   132  func (j diskJournal) readOrdinalFromDisk(path string) (journalOrdinal, error) {
   133  	buf, err := ioutil.ReadFile(path)
   134  	if err != nil {
   135  		return 0, err
   136  	}
   137  	return makeJournalOrdinal(string(buf))
   138  }
   139  
   140  func (j *diskJournal) writeOrdinalToDisk(path string, o journalOrdinal) error {
   141  	return ioutil.WriteSerializedFile(path, []byte(o.String()), 0600)
   142  }
   143  
   144  func (j diskJournal) readEarliestOrdinalFromDisk() (journalOrdinal, error) {
   145  	return j.readOrdinalFromDisk(j.earliestPath())
   146  }
   147  
   148  func (j diskJournal) readLatestOrdinalFromDisk() (journalOrdinal, error) {
   149  	return j.readOrdinalFromDisk(j.latestPath())
   150  }
   151  
   152  func (j diskJournal) empty() bool {
   153  	return !j.earliestValid || !j.latestValid
   154  }
   155  
   156  // TODO: Change {read,write}{Earliest,Latest}Ordinal() to
   157  // {get,set}{Earliest,Latest}Ordinal(), and have the getters return an
   158  // isValid bool, or an invalid journalOrdinal instead of an error.
   159  
   160  func (j diskJournal) readEarliestOrdinal() (journalOrdinal, error) {
   161  	if j.empty() {
   162  		return journalOrdinal(0), errors.WithStack(os.ErrNotExist)
   163  	}
   164  	return j.earliest, nil
   165  }
   166  
   167  func (j *diskJournal) writeEarliestOrdinal(o journalOrdinal) error {
   168  	err := j.writeOrdinalToDisk(j.earliestPath(), o)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	j.earliestValid = true
   173  	j.earliest = o
   174  	return nil
   175  }
   176  
   177  func (j diskJournal) readLatestOrdinal() (journalOrdinal, error) {
   178  	if j.empty() {
   179  		return journalOrdinal(0), errors.WithStack(os.ErrNotExist)
   180  	}
   181  	return j.latest, nil
   182  }
   183  
   184  func (j *diskJournal) writeLatestOrdinal(o journalOrdinal) error {
   185  	err := j.writeOrdinalToDisk(j.latestPath(), o)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	j.latestValid = true
   190  	j.latest = o
   191  	return nil
   192  }
   193  
   194  // clear completely removes the journal directory.
   195  func (j *diskJournal) clear() error {
   196  	// Clear ordinals first to not leave the journal in a weird
   197  	// state if we crash in the middle of removing the files,
   198  	// assuming that file removal is atomic.
   199  	err := ioutil.Remove(j.earliestPath())
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	// If we crash here, on the next startup the journal will
   205  	// still be considered empty.
   206  
   207  	j.earliestValid = false
   208  	j.earliest = journalOrdinal(0)
   209  
   210  	err = ioutil.Remove(j.latestPath())
   211  	if err != nil {
   212  		return err
   213  	}
   214  
   215  	j.latestValid = false
   216  	j.latest = journalOrdinal(0)
   217  
   218  	// j.dir will be recreated on the next call to
   219  	// writeJournalEntry (via kbfscodec.SerializeToFile), which
   220  	// must always come before any ordinal write.
   221  	return ioutil.RemoveAll(j.dir)
   222  }
   223  
   224  // removeEarliest removes the earliest entry in the journal. If that
   225  // entry was the last one, clear() is also called, and true is
   226  // returned.
   227  func (j *diskJournal) removeEarliest() (empty bool, err error) {
   228  	if j.empty() {
   229  		// TODO: Return a more meaningful error.
   230  		return false, errors.WithStack(os.ErrNotExist)
   231  	}
   232  
   233  	if j.earliest == j.latest {
   234  		err := j.clear()
   235  		if err != nil {
   236  			return false, err
   237  		}
   238  		return true, nil
   239  	}
   240  
   241  	oldEarliest := j.earliest
   242  
   243  	err = j.writeEarliestOrdinal(oldEarliest + 1)
   244  	if err != nil {
   245  		return false, err
   246  	}
   247  
   248  	// Garbage-collect the old entry. If we crash here and leave
   249  	// behind an entry, it'll be cleaned up the next time clear()
   250  	// is called.
   251  	p := j.journalEntryPath(oldEarliest)
   252  	err = ioutil.Remove(p)
   253  	if err != nil {
   254  		return false, err
   255  	}
   256  
   257  	return false, nil
   258  }
   259  
   260  // The functions below are for reading and writing journal entries.
   261  
   262  func (j diskJournal) readJournalEntry(o journalOrdinal) (interface{}, error) {
   263  	p := j.journalEntryPath(o)
   264  	entry := reflect.New(j.entryType)
   265  	err := kbfscodec.DeserializeFromFile(j.codec, p, entry)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  
   270  	return entry.Elem().Interface(), nil
   271  }
   272  
   273  func (j *diskJournal) writeJournalEntry(
   274  	o journalOrdinal, entry interface{}) error {
   275  	entryType := reflect.TypeOf(entry)
   276  	if entryType != j.entryType {
   277  		panic(errors.Errorf("Expected entry type %v, got %v",
   278  			j.entryType, entryType))
   279  	}
   280  
   281  	return kbfscodec.SerializeToFile(j.codec, entry, j.journalEntryPath(o))
   282  }
   283  
   284  // appendJournalEntry appends the given entry to the journal. If o is
   285  // nil, then if the journal is empty, the new entry will have ordinal
   286  // 0, and otherwise it will have ordinal equal to the successor of the
   287  // latest ordinal. Otherwise, if o is non-nil, then if the journal is
   288  // empty, the new entry will have ordinal *o, and otherwise it returns
   289  // an error if *o is not the successor of the latest ordinal. If
   290  // successful, appendJournalEntry returns the ordinal of the
   291  // just-appended entry.
   292  func (j *diskJournal) appendJournalEntry(
   293  	o *journalOrdinal, entry interface{}) (journalOrdinal, error) {
   294  	var next journalOrdinal
   295  	if j.empty() {
   296  		if o != nil {
   297  			next = *o
   298  		} else {
   299  			next = firstValidJournalOrdinal
   300  		}
   301  	} else {
   302  		next = j.latest + 1
   303  		if next == 0 {
   304  			// Rollover is almost certainly a bug.
   305  			return 0, errors.Errorf(
   306  				"Ordinal rollover for %+v", entry)
   307  		}
   308  		if o != nil && next != *o {
   309  			return 0, errors.Errorf(
   310  				"%v unexpectedly does not follow %v for %+v",
   311  				*o, j.latest, entry)
   312  		}
   313  	}
   314  
   315  	err := j.writeJournalEntry(next, entry)
   316  	if err != nil {
   317  		return 0, err
   318  	}
   319  
   320  	if j.empty() {
   321  		err := j.writeEarliestOrdinal(next)
   322  		if err != nil {
   323  			return 0, err
   324  		}
   325  	}
   326  	err = j.writeLatestOrdinal(next)
   327  	if err != nil {
   328  		return 0, err
   329  	}
   330  	return next, nil
   331  }
   332  
   333  // move moves the journal to the given directory, which should share
   334  // the same parent directory as the current journal directory.
   335  func (j *diskJournal) move(newDir string) (oldDir string, err error) {
   336  	err = ioutil.Rename(j.dir, newDir)
   337  	if err != nil && !ioutil.IsNotExist(err) {
   338  		return "", err
   339  	}
   340  	oldDir = j.dir
   341  	j.dir = newDir
   342  	return oldDir, nil
   343  }
   344  
   345  func (j diskJournal) length() uint64 {
   346  	if j.empty() {
   347  		return 0
   348  	}
   349  	return uint64(j.latest - j.earliest + 1)
   350  }