github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/ldbutils/leveldb.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 ldbutils
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/keybase/client/go/kbfs/ioutil"
    17  	"github.com/keybase/client/go/logger"
    18  	"github.com/pkg/errors"
    19  	"github.com/syndtr/goleveldb/leveldb"
    20  	ldberrors "github.com/syndtr/goleveldb/leveldb/errors"
    21  	"github.com/syndtr/goleveldb/leveldb/filter"
    22  	"github.com/syndtr/goleveldb/leveldb/opt"
    23  	"github.com/syndtr/goleveldb/leveldb/storage"
    24  )
    25  
    26  const (
    27  	diskCacheVersionFilename string = "version"
    28  	// Metered specified that this DB should be metered.
    29  	Metered = true
    30  	// Unmetered specified that this DB should not be metered.
    31  	Unmetered = false
    32  )
    33  
    34  // DbWriteBufferSizeGetter is an interface that contains a method for
    35  // getting the size of a leveldb write buffer.
    36  type DbWriteBufferSizeGetter interface {
    37  	// DbWriteBufferSize indicates how large the write buffer should
    38  	// be on local levelDbs -- this also controls how big the on-disk
    39  	// tables are before compaction.
    40  	DbWriteBufferSize() int
    41  }
    42  
    43  var leveldbOptions = &opt.Options{
    44  	Compression: opt.NoCompression,
    45  	WriteBuffer: 10 * opt.MiB,
    46  	BlockSize:   1 << 16,
    47  	// Default max open file descriptors (ulimit -n) is 256 on OS
    48  	// X, and >=1024 on (most?) Linux machines. So set to a low
    49  	// number since we have multiple leveldb instances.
    50  	OpenFilesCacheCapacity: 10,
    51  }
    52  
    53  // LeveldbOptions returns leveldb options.
    54  func LeveldbOptions(sizeGetter DbWriteBufferSizeGetter) *opt.Options {
    55  	o := *leveldbOptions
    56  	if sizeGetter != nil {
    57  		o.WriteBuffer = sizeGetter.DbWriteBufferSize()
    58  	}
    59  	return &o
    60  }
    61  
    62  // LevelDb is a libkbfs wrapper for leveldb.DB.
    63  type LevelDb struct {
    64  	*leveldb.DB
    65  	closer io.Closer
    66  }
    67  
    68  // Close closes the DB.
    69  func (ldb *LevelDb) Close() (err error) {
    70  	err = ldb.DB.Close()
    71  	// Hide the closer error.
    72  	_ = ldb.closer.Close()
    73  	return err
    74  }
    75  
    76  // Get gets data from the DB.
    77  func (ldb *LevelDb) Get(key []byte, ro *opt.ReadOptions) (
    78  	value []byte, err error) {
    79  	defer func() {
    80  		if err != nil {
    81  			err = errors.WithStack(err)
    82  		}
    83  	}()
    84  	return ldb.DB.Get(key, ro)
    85  }
    86  
    87  // GetWithMeter gets data from the DB while tracking the hit rate.
    88  func (ldb *LevelDb) GetWithMeter(key []byte, hitMeter, missMeter *CountMeter) (
    89  	value []byte, err error) {
    90  	defer func() {
    91  		if err == nil {
    92  			if hitMeter != nil {
    93  				hitMeter.Mark(1)
    94  			}
    95  		} else if missMeter != nil {
    96  			missMeter.Mark(1)
    97  		}
    98  	}()
    99  	return ldb.Get(key, nil)
   100  }
   101  
   102  // Put puts data into the DB.
   103  func (ldb *LevelDb) Put(key, value []byte, wo *opt.WriteOptions) (err error) {
   104  	defer func() {
   105  		if err != nil {
   106  			err = errors.WithStack(err)
   107  		}
   108  	}()
   109  	return ldb.DB.Put(key, value, wo)
   110  }
   111  
   112  // PutWithMeter gets data from the DB while tracking the hit rate.
   113  func (ldb *LevelDb) PutWithMeter(key, value []byte, putMeter *CountMeter) (
   114  	err error) {
   115  	defer func() {
   116  		if err == nil && putMeter != nil {
   117  			putMeter.Mark(1)
   118  		}
   119  	}()
   120  	return ldb.Put(key, value, nil)
   121  }
   122  
   123  // StatStrings returns newline-split leveldb stats, suitable for JSONification.
   124  func (ldb *LevelDb) StatStrings() ([]string, error) {
   125  	stats, err := ldb.GetProperty("leveldb.stats")
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	return strings.Split(stats, "\n"), nil
   130  }
   131  
   132  // OpenLevelDbWithOptions opens or recovers a leveldb.DB with a
   133  // passed-in storage.Storage as its underlying storage layer, and with
   134  // the options specified.
   135  func OpenLevelDbWithOptions(stor storage.Storage, options *opt.Options) (
   136  	*LevelDb, error) {
   137  	db, err := leveldb.Open(stor, options)
   138  	if ldberrors.IsCorrupted(err) {
   139  		// There's a possibility that if the leveldb wasn't closed properly
   140  		// last time while it was being written, then the manifest is corrupt.
   141  		// This means leveldb must rebuild its manifest, which takes longer
   142  		// than a simple `Open`.
   143  		// TODO: log here
   144  		db, err = leveldb.Recover(stor, options)
   145  	}
   146  	if err != nil {
   147  		stor.Close()
   148  		return nil, err
   149  	}
   150  	return &LevelDb{db, stor}, nil
   151  }
   152  
   153  // OpenLevelDb opens or recovers a leveldb.DB with a passed-in
   154  // storage.Storage as its underlying storage layer.
   155  func OpenLevelDb(
   156  	stor storage.Storage, sizeGetter DbWriteBufferSizeGetter) (
   157  	*LevelDb, error) {
   158  	options := LeveldbOptions(sizeGetter)
   159  	options.Filter = filter.NewBloomFilter(16)
   160  	return OpenLevelDbWithOptions(stor, options)
   161  }
   162  
   163  func versionPathFromVersion(dirPath string, version uint64) string {
   164  	return filepath.Join(dirPath, fmt.Sprintf("v%d", version))
   165  }
   166  
   167  // GetVersionedPathForDb returns a path for the db that includes a
   168  // version number.
   169  func GetVersionedPathForDb(
   170  	log logger.Logger, dirPath string, dbName string,
   171  	currentDbVersion uint64) (versionedDirPath string, err error) {
   172  	// Read the version file
   173  	versionFilepath := filepath.Join(dirPath, diskCacheVersionFilename)
   174  	versionBytes, err := ioutil.ReadFile(versionFilepath)
   175  	// We expect the file to open successfully or not exist. Anything else is a
   176  	// problem.
   177  	version := currentDbVersion
   178  	switch {
   179  	case ioutil.IsNotExist(err):
   180  		// Do nothing, meaning that we will create the version file below.
   181  		log.CDebugf(
   182  			context.TODO(), "Creating new version file for the %s DB.",
   183  			dbName)
   184  	case err != nil:
   185  		log.CDebugf(
   186  			context.TODO(),
   187  			"An error occurred while reading the %s DB "+
   188  				"version file. Using %d as the version and creating a new "+
   189  				"file to record it.", dbName, version)
   190  		// TODO: when we increase the version of the DB, we'll have to
   191  		// make sure we wipe all previous versions of the DB.
   192  	default:
   193  		// We expect a successfully opened version file to parse a
   194  		// single unsigned integer representing the version. Anything
   195  		// else is a corrupted version file. However, this we can
   196  		// solve by deleting everything in the cache.  TODO:
   197  		// Eventually delete the whole DB if we have an out of date
   198  		// version.
   199  		version, err = strconv.ParseUint(string(versionBytes), 10,
   200  			strconv.IntSize)
   201  		if err == nil && version == currentDbVersion {
   202  			// Success case, no need to write the version file again.
   203  			log.CDebugf(
   204  				context.TODO(),
   205  				"Loaded the %s DB version file successfully."+
   206  					" Version: %d", dbName, version)
   207  			return versionPathFromVersion(dirPath, version), nil
   208  		}
   209  		switch {
   210  		case err != nil:
   211  			log.CDebugf(
   212  				context.TODO(),
   213  				"An error occurred while parsing the %s DB "+
   214  					"version file. Using %d as the version.",
   215  				dbName, currentDbVersion)
   216  			// TODO: when we increase the version of the DB, we'll have
   217  			// to make sure we wipe all previous versions of the DB.
   218  			version = currentDbVersion
   219  		case version < currentDbVersion:
   220  			log.CDebugf(
   221  				context.TODO(),
   222  				"The %s DB version file contained an old "+
   223  					"version: %d. Updating to the new version: %d.",
   224  				dbName, version, currentDbVersion)
   225  			// TODO: when we increase the version of the DB, we'll have
   226  			// to make sure we wipe all previous versions of the DB.
   227  			version = currentDbVersion
   228  		case version > currentDbVersion:
   229  			log.CDebugf(
   230  				context.TODO(),
   231  				"The %s DB version file contained a newer "+
   232  					"version (%d) than this client knows how to read. "+
   233  					"Switching to this client's newest known version: %d.",
   234  				dbName, version, currentDbVersion)
   235  			version = currentDbVersion
   236  		}
   237  	}
   238  	// Ensure the DB directory exists.
   239  	err = os.MkdirAll(dirPath, 0700)
   240  	if err != nil {
   241  		// This does actually need to be fatal.
   242  		return "", err
   243  	}
   244  	versionString := strconv.FormatUint(version, 10)
   245  	err = ioutil.WriteFile(versionFilepath, []byte(versionString), 0600)
   246  	if err != nil {
   247  		// This also needs to be fatal.
   248  		return "", err
   249  	}
   250  	return versionPathFromVersion(dirPath, version), nil
   251  }
   252  
   253  // OpenVersionedLevelDb opens a level DB under a versioned path on the
   254  // local filesystem under storageRoot. The path include dbFolderName
   255  // and dbFilename. Note that dbFilename is actually created as a
   256  // folder; it's just where raw LevelDb lives.
   257  func OpenVersionedLevelDb(
   258  	log logger.Logger, storageRoot string, dbFolderName string,
   259  	currentDbVersion uint64, dbFilename string,
   260  	sizeGetter DbWriteBufferSizeGetter) (db *LevelDb, err error) {
   261  	dbPath := filepath.Join(storageRoot, dbFolderName)
   262  	versionPath, err := GetVersionedPathForDb(
   263  		log, dbPath, dbFolderName, currentDbVersion)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	p := filepath.Join(versionPath, dbFilename)
   268  	log.Debug("opening LevelDB: %s", p)
   269  	storage, err := storage.OpenFile(p, false)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	defer func() {
   274  		if err != nil {
   275  			storage.Close()
   276  		}
   277  	}()
   278  	options := LeveldbOptions(sizeGetter)
   279  	if db, err = OpenLevelDbWithOptions(storage, options); err != nil {
   280  		return nil, err
   281  	}
   282  	return db, nil
   283  }