dir_plan9.go raw

   1  /*
   2   * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
   3   * SPDX-License-Identifier: Apache-2.0
   4   */
   5  
   6  package badger
   7  
   8  import (
   9  	"fmt"
  10  	"os"
  11  	"path/filepath"
  12  	"strings"
  13  
  14  	"github.com/dgraph-io/badger/v4/y"
  15  )
  16  
  17  // directoryLockGuard holds a lock on a directory and a pid file inside.  The pid file isn't part
  18  // of the locking mechanism, it's just advisory.
  19  type directoryLockGuard struct {
  20  	// File handle on the directory, which we've locked.
  21  	f *os.File
  22  	// The absolute path to our pid file.
  23  	path string
  24  }
  25  
  26  // acquireDirectoryLock gets a lock on the directory.
  27  // It will also write our pid to dirPath/pidFileName for convenience.
  28  // readOnly is not supported on Plan 9.
  29  func acquireDirectoryLock(dirPath string, pidFileName string, readOnly bool) (
  30  	*directoryLockGuard, error) {
  31  	if readOnly {
  32  		return nil, ErrPlan9NotSupported
  33  	}
  34  
  35  	// Convert to absolute path so that Release still works even if we do an unbalanced
  36  	// chdir in the meantime.
  37  	absPidFilePath, err := filepath.Abs(filepath.Join(dirPath, pidFileName))
  38  	if err != nil {
  39  		return nil, y.Wrap(err, "cannot get absolute path for pid lock file")
  40  	}
  41  
  42  	// If the file was unpacked or created by some other program, it might not
  43  	// have the ModeExclusive bit set. Set it before we call OpenFile, so that we
  44  	// can be confident that a successful OpenFile implies exclusive use.
  45  	//
  46  	// OpenFile fails if the file ModeExclusive bit set *and* the file is already open.
  47  	// So, if the file is closed when the DB crashed, we're fine. When the process
  48  	// that was managing the DB crashes, the OS will close the file for us.
  49  	//
  50  	// This bit of code is copied from Go's lockedfile internal package:
  51  	// https://github.com/golang/go/blob/go1.15rc1/src/cmd/go/internal/lockedfile/lockedfile_plan9.go#L58
  52  	if fi, err := os.Stat(absPidFilePath); err == nil {
  53  		if fi.Mode()&os.ModeExclusive == 0 {
  54  			if err := os.Chmod(absPidFilePath, fi.Mode()|os.ModeExclusive); err != nil {
  55  				return nil, y.Wrapf(err, "could not set exclusive mode bit")
  56  			}
  57  		}
  58  	} else if !os.IsNotExist(err) {
  59  		return nil, err
  60  	}
  61  	f, err := os.OpenFile(absPidFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666|os.ModeExclusive)
  62  	if err != nil {
  63  		if isLocked(err) {
  64  			return nil, y.Wrapf(err,
  65  				"Cannot open pid lock file %q.  Another process is using this Badger database",
  66  				absPidFilePath)
  67  		}
  68  		return nil, y.Wrapf(err, "Cannot open pid lock file %q", absPidFilePath)
  69  	}
  70  
  71  	if _, err = fmt.Fprintf(f, "%d\n", os.Getpid()); err != nil {
  72  		f.Close()
  73  		return nil, y.Wrapf(err, "could not write pid")
  74  	}
  75  	return &directoryLockGuard{f, absPidFilePath}, nil
  76  }
  77  
  78  // Release deletes the pid file and releases our lock on the directory.
  79  func (guard *directoryLockGuard) release() error {
  80  	// It's important that we remove the pid file first.
  81  	err := os.Remove(guard.path)
  82  
  83  	if closeErr := guard.f.Close(); err == nil {
  84  		err = closeErr
  85  	}
  86  	guard.path = ""
  87  	guard.f = nil
  88  
  89  	return err
  90  }
  91  
  92  // openDir opens a directory for syncing.
  93  func openDir(path string) (*os.File, error) { return os.Open(path) }
  94  
  95  // When you create or delete a file, you have to ensure the directory entry for the file is synced
  96  // in order to guarantee the file is visible (if the system crashes). (See the man page for fsync,
  97  // or see https://github.com/coreos/etcd/issues/6368 for an example.)
  98  func syncDir(dir string) error {
  99  	f, err := openDir(dir)
 100  	if err != nil {
 101  		return y.Wrapf(err, "While opening directory: %s.", dir)
 102  	}
 103  
 104  	err = f.Sync()
 105  	closeErr := f.Close()
 106  	if err != nil {
 107  		return y.Wrapf(err, "While syncing directory: %s.", dir)
 108  	}
 109  	return y.Wrapf(closeErr, "While closing directory: %s.", dir)
 110  }
 111  
 112  // Opening an exclusive-use file returns an error.
 113  // The expected error strings are:
 114  //
 115  //   - "open/create -- file is locked" (cwfs, kfs)
 116  //   - "exclusive lock" (fossil)
 117  //   - "exclusive use file already open" (ramfs)
 118  //
 119  // See https://github.com/golang/go/blob/go1.15rc1/src/cmd/go/internal/lockedfile/lockedfile_plan9.go#L16
 120  var lockedErrStrings = [...]string{
 121  	"file is locked",
 122  	"exclusive lock",
 123  	"exclusive use file already open",
 124  }
 125  
 126  // Even though plan9 doesn't support the Lock/RLock/Unlock functions to
 127  // manipulate already-open files, IsLocked is still meaningful: os.OpenFile
 128  // itself may return errors that indicate that a file with the ModeExclusive bit
 129  // set is already open.
 130  func isLocked(err error) bool {
 131  	s := err.Error()
 132  
 133  	for _, frag := range lockedErrStrings {
 134  		if strings.Contains(s, frag) {
 135  			return true
 136  		}
 137  	}
 138  	return false
 139  }
 140