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 }