dir_unix.go raw

   1  //go:build !windows && !plan9 && !js && !wasip1
   2  // +build !windows,!plan9,!js,!wasip1
   3  
   4  /*
   5   * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
   6   * SPDX-License-Identifier: Apache-2.0
   7   */
   8  
   9  package badger
  10  
  11  import (
  12  	"fmt"
  13  	"os"
  14  	"path/filepath"
  15  
  16  	"golang.org/x/sys/unix"
  17  
  18  	"github.com/dgraph-io/badger/v4/y"
  19  )
  20  
  21  // directoryLockGuard holds a lock on a directory and a pid file inside.  The pid file isn't part
  22  // of the locking mechanism, it's just advisory.
  23  type directoryLockGuard struct {
  24  	// File handle on the directory, which we've flocked.
  25  	f *os.File
  26  	// The absolute path to our pid file.
  27  	path string
  28  	// Was this a shared lock for a read-only database?
  29  	readOnly bool
  30  }
  31  
  32  // acquireDirectoryLock gets a lock on the directory (using flock). If
  33  // this is not read-only, it will also write our pid to
  34  // dirPath/pidFileName for convenience.
  35  func acquireDirectoryLock(dirPath string, pidFileName string, readOnly bool) (
  36  	*directoryLockGuard, error) {
  37  	// Convert to absolute path so that Release still works even if we do an unbalanced
  38  	// chdir in the meantime.
  39  	absPidFilePath, err := filepath.Abs(filepath.Join(dirPath, pidFileName))
  40  	if err != nil {
  41  		return nil, y.Wrapf(err, "cannot get absolute path for pid lock file")
  42  	}
  43  	f, err := os.Open(dirPath)
  44  	if err != nil {
  45  		return nil, y.Wrapf(err, "cannot open directory %q", dirPath)
  46  	}
  47  	opts := unix.LOCK_EX | unix.LOCK_NB
  48  	if readOnly {
  49  		opts = unix.LOCK_SH | unix.LOCK_NB
  50  	}
  51  
  52  	err = unix.Flock(int(f.Fd()), opts)
  53  	if err != nil {
  54  		f.Close()
  55  		return nil, y.Wrapf(err,
  56  			"Cannot acquire directory lock on %q.  Another process is using this Badger database.",
  57  			dirPath)
  58  	}
  59  
  60  	if !readOnly {
  61  		// Yes, we happily overwrite a pre-existing pid file.  We're the
  62  		// only read-write badger process using this directory.
  63  		err = os.WriteFile(absPidFilePath, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0666)
  64  		if err != nil {
  65  			f.Close()
  66  			return nil, y.Wrapf(err,
  67  				"Cannot write pid file %q", absPidFilePath)
  68  		}
  69  	}
  70  	return &directoryLockGuard{f, absPidFilePath, readOnly}, nil
  71  }
  72  
  73  // Release deletes the pid file and releases our lock on the directory.
  74  func (guard *directoryLockGuard) release() error {
  75  	var err error
  76  	if !guard.readOnly {
  77  		// It's important that we remove the pid file first.
  78  		err = os.Remove(guard.path)
  79  	}
  80  
  81  	if closeErr := guard.f.Close(); err == nil {
  82  		err = closeErr
  83  	}
  84  	guard.path = ""
  85  	guard.f = nil
  86  
  87  	return err
  88  }
  89  
  90  // openDir opens a directory for syncing.
  91  func openDir(path string) (*os.File, error) { return os.Open(path) }
  92  
  93  // When you create or delete a file, you have to ensure the directory entry for the file is synced
  94  // in order to guarantee the file is visible (if the system crashes). (See the man page for fsync,
  95  // or see https://github.com/coreos/etcd/issues/6368 for an example.)
  96  func syncDir(dir string) error {
  97  	f, err := openDir(dir)
  98  	if err != nil {
  99  		return y.Wrapf(err, "While opening directory: %s.", dir)
 100  	}
 101  
 102  	err = f.Sync()
 103  	closeErr := f.Close()
 104  	if err != nil {
 105  		return y.Wrapf(err, "While syncing directory: %s.", dir)
 106  	}
 107  	return y.Wrapf(closeErr, "While closing directory: %s.", dir)
 108  }
 109