github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/libkb/leveldb.go (about) 1 // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package libkb 5 6 import ( 7 "fmt" 8 "os" 9 "path/filepath" 10 "strings" 11 "sync" 12 13 "github.com/syndtr/goleveldb/leveldb" 14 errors "github.com/syndtr/goleveldb/leveldb/errors" 15 "github.com/syndtr/goleveldb/leveldb/filter" 16 "github.com/syndtr/goleveldb/leveldb/opt" 17 "github.com/syndtr/goleveldb/leveldb/util" 18 "golang.org/x/net/context" 19 ) 20 21 // table names 22 const ( 23 levelDbTableLo = "lo" 24 levelDbTableKv = "kv" 25 // keys with this prefix are ignored by the dbcleaner 26 levelDbTablePerm = "pm" 27 ) 28 29 type levelDBOps interface { 30 Delete(key []byte, wo *opt.WriteOptions) error 31 Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) 32 Put(key, value []byte, wo *opt.WriteOptions) error 33 Write(b *leveldb.Batch, wo *opt.WriteOptions) error 34 } 35 36 func levelDbPut(ops levelDBOps, cleaner *levelDbCleaner, id DbKey, aliases []DbKey, value []byte) (err error) { 37 defer convertNoSpaceError(&err) 38 39 idb := id.ToBytes() 40 if aliases == nil { 41 // if no aliases, just do a put 42 if err := ops.Put(idb, value, nil); err != nil { 43 return err 44 } 45 cleaner.markRecentlyUsed(context.Background(), idb) 46 return nil 47 } 48 49 batch := new(leveldb.Batch) 50 batch.Put(idb, value) 51 keys := make([][]byte, len(aliases)) 52 keys = append(keys, idb) 53 for i, alias := range aliases { 54 aliasKey := alias.ToBytesLookup() 55 batch.Put(aliasKey, idb) 56 keys[i] = aliasKey 57 } 58 59 if err := ops.Write(batch, nil); err != nil { 60 return err 61 } 62 for _, key := range keys { 63 cleaner.markRecentlyUsed(context.Background(), key) 64 } 65 return nil 66 } 67 68 func levelDbGetWhich(ops levelDBOps, cleaner *levelDbCleaner, key []byte) (val []byte, found bool, err error) { 69 val, err = ops.Get(key, nil) 70 found = false 71 if err == nil { 72 found = true 73 } else if err == leveldb.ErrNotFound { 74 err = nil 75 } 76 77 if found && err == nil { 78 cleaner.markRecentlyUsed(context.Background(), key) 79 } 80 return val, found, err 81 } 82 83 func levelDbGet(ops levelDBOps, cleaner *levelDbCleaner, id DbKey) ([]byte, bool, error) { 84 return levelDbGetWhich(ops, cleaner, id.ToBytes()) 85 } 86 87 func levelDbLookup(ops levelDBOps, cleaner *levelDbCleaner, id DbKey) (val []byte, found bool, err error) { 88 val, found, err = levelDbGetWhich(ops, cleaner, id.ToBytesLookup()) 89 if found { 90 if tab, id2, err2 := DbKeyParse(string(val)); err2 != nil { 91 err = err2 92 } else if tab != levelDbTableKv && tab != levelDbTablePerm { 93 err = fmt.Errorf("bad alias; expected 'kv' or 'pm' but got '%s'", tab) 94 } else { 95 val, found, err = levelDbGetWhich(ops, cleaner, id2.ToBytes()) 96 } 97 } 98 return val, found, err 99 } 100 101 func levelDbDelete(ops levelDBOps, cleaner *levelDbCleaner, id DbKey) (err error) { 102 defer convertNoSpaceError(&err) 103 key := id.ToBytes() 104 if err := ops.Delete(key, nil); err != nil { 105 return err 106 } 107 108 cleaner.removeRecentlyUsed(context.Background(), key) 109 return nil 110 } 111 112 type LevelDb struct { 113 // We use a RWMutex here to ensure close doesn't happen in the middle of 114 // other DB operations, and DB operations doesn't happen after close. The 115 // lock should be considered for the db pointer and dbOpenerOnce pointer, 116 // rather than the DB itself. More specifically, close does Lock(), while 117 // other DB operations does RLock(). 118 sync.RWMutex 119 db *leveldb.DB 120 dbOpenerOnce *sync.Once 121 cleaner *levelDbCleaner 122 123 filename string 124 Contextified 125 } 126 127 func NewLevelDb(g *GlobalContext, filename func() string) *LevelDb { 128 path := filename() 129 return &LevelDb{ 130 Contextified: NewContextified(g), 131 filename: path, 132 dbOpenerOnce: new(sync.Once), 133 cleaner: newLevelDbCleaner(NewMetaContextTODO(g), filepath.Base(path)), 134 } 135 } 136 137 // Explicit open does nothing we'll wait for a lazy open 138 func (l *LevelDb) Open() error { return nil } 139 140 // Opts returns the options for all leveldb databases. 141 // 142 // PC: I think it's worth trying a bloom filter. From docs: 143 // "In many cases, a filter can cut down the number of disk 144 // seeks from a handful to a single disk seek per DB.Get call." 145 func (l *LevelDb) Opts() *opt.Options { 146 return &opt.Options{ 147 OpenFilesCacheCapacity: l.G().Env.GetLevelDBNumFiles(), 148 Filter: filter.NewBloomFilter(10), 149 CompactionTableSize: 10 * opt.MiB, 150 WriteBuffer: l.G().Env.GetLevelDBWriteBufferMB() * opt.MiB, 151 } 152 } 153 154 func (l *LevelDb) doWhileOpenAndNukeIfCorrupted(action func() error) (err error) { 155 err = func() error { 156 l.RLock() 157 defer l.RUnlock() 158 159 // This only happens at first ever doWhileOpenAndNukeIfCorrupted call, or 160 // when doOpenerOnce is just reset in Nuke() 161 l.dbOpenerOnce.Do(func() { 162 l.G().Log.Debug("+ LevelDb.open") 163 fn := l.GetFilename() 164 l.G().Log.Debug("| Opening LevelDB for local cache: %v %s", l, fn) 165 l.G().Log.Debug("| Opening LevelDB options: %+v", l.Opts()) 166 l.db, err = leveldb.OpenFile(fn, l.Opts()) 167 if _, ok := err.(*errors.ErrCorrupted); ok { 168 l.G().Log.Debug("| LevelDb was corrupted; attempting recovery (%v)", err) 169 var recoveryError error 170 l.db, recoveryError = leveldb.RecoverFile(fn, nil) 171 if recoveryError != nil { 172 l.G().Log.Debug("| Recovery failed: %v", recoveryError) 173 } else { 174 l.G().Log.Debug("| Recovery succeeded!") 175 // wipe the outer error since it's fixed now 176 err = nil 177 } 178 } 179 l.G().Log.Debug("- LevelDb.open -> %s", ErrToOk(err)) 180 if l.db != nil { 181 l.cleaner.setDb(l.db) 182 } 183 }) 184 185 if err != nil { 186 return err 187 } 188 189 if l.db == nil { 190 // This means DB is already closed. We are preventing lazy-opening after 191 // closing, so just return error here. 192 return LevelDBOpenClosedError{} 193 } 194 195 return action() 196 }() 197 198 // If the file is corrupt, just nuke and act like we didn't find anything 199 if l.nukeIfCorrupt(err) { 200 err = nil 201 } else if IsNoSpaceOnDeviceError(err) { 202 // If we are out of space force a db clean 203 go func() { _ = l.cleaner.clean(true) }() 204 } 205 206 // Notably missing here is the error handling for when DB open fails but on 207 // an error other than "db is corrupted". We simply return the error here 208 // without resetting `dbOpenerOnce` (i.e. next call into LevelDb would result 209 // in a LevelDBOpenClosedError), because if DB open fails, retrying it 210 // wouldn't help. We should find the root cause and deal with it. 211 // MM: 10/12/2017: I am changing the above policy. I am not so sure retrying it won't help, 212 // we should at least try instead of auto returning LevelDBOpenClosederror. 213 if err != nil { 214 l.Lock() 215 if l.db == nil { 216 l.G().Log.Debug("LevelDb: doWhileOpenAndNukeIfCorrupted: resetting sync one: %s", err) 217 l.dbOpenerOnce = new(sync.Once) 218 } 219 l.Unlock() 220 } 221 return err 222 } 223 224 // ForceOpen opens the leveldb file. This is used in situations 225 // where we want to get around the lazy open and make sure we can 226 // use it later. 227 func (l *LevelDb) ForceOpen() error { 228 return l.doWhileOpenAndNukeIfCorrupted(func() error { return nil }) 229 } 230 231 func (l *LevelDb) Stats() (stats string) { 232 if err := l.doWhileOpenAndNukeIfCorrupted(func() (err error) { 233 stats, err = l.db.GetProperty("leveldb.stats") 234 stats = fmt.Sprintf("%s\n%s", stats, l.cleaner.Status()) 235 return err 236 }); err != nil { 237 return "" 238 } 239 return stats 240 } 241 242 func (l *LevelDb) CompactionStats() (memActive, tableActive bool, err error) { 243 var dbStats leveldb.DBStats 244 if err := l.doWhileOpenAndNukeIfCorrupted(func() (err error) { 245 return l.db.Stats(&dbStats) 246 }); err != nil { 247 return false, false, err 248 } 249 return dbStats.MemCompactionActive, dbStats.TableCompactionActive, nil 250 } 251 252 func (l *LevelDb) GetFilename() string { 253 if len(l.filename) == 0 { 254 l.G().Log.Fatalf("DB filename empty") 255 } 256 return l.filename 257 } 258 259 func (l *LevelDb) Close() error { 260 l.Lock() 261 defer l.Unlock() 262 return l.closeLocked() 263 } 264 265 func (l *LevelDb) closeLocked() error { 266 var err error 267 if l.db != nil { 268 l.G().Log.Debug("Closing LevelDB local cache: %s", l.GetFilename()) 269 err = l.db.Close() 270 l.db = nil 271 272 // In case we just nuked DB and reset the dbOpenerOnce, this makes sure it 273 // doesn't open the DB again. 274 l.dbOpenerOnce.Do(func() {}) 275 // stop any active cleaning jobs 276 l.cleaner.Stop() 277 l.cleaner.Shutdown() 278 } 279 return err 280 } 281 282 func (l *LevelDb) isCorrupt(err error) bool { 283 if err == nil { 284 return false 285 } 286 287 // If the error is of type ErrCorrupted, then we nuke 288 if _, ok := err.(*errors.ErrCorrupted); ok { 289 return true 290 } 291 292 // Sometimes the LevelDB library will return generic error messages about 293 // corruption, also nuke on them 294 if strings.Contains(err.Error(), "corrupt") { 295 return true 296 } 297 return false 298 } 299 300 func (l *LevelDb) Clean(force bool) (err error) { 301 l.Lock() 302 defer l.Unlock() 303 defer l.G().Trace("LevelDb::Clean", &err)() 304 return l.cleaner.clean(force) 305 } 306 307 func (l *LevelDb) Nuke() (fn string, err error) { 308 l.Lock() 309 // We need to do deferred Unlock here in Nuke rather than delegating to 310 // l.Close() because we'll be re-opening the database later, and it's 311 // necessary to block other doWhileOpenAndNukeIfCorrupted() calls. 312 defer l.Unlock() 313 defer l.G().Trace("LevelDb::Nuke", &err)() 314 315 // even if we can't close the db try to nuke the files directly 316 if err = l.closeLocked(); err != nil { 317 l.G().Log.Debug("Error closing leveldb %v, attempting nuke anyway", err) 318 } 319 320 fn = l.GetFilename() 321 if err = os.RemoveAll(fn); err != nil { 322 return fn, err 323 } 324 // reset dbOpenerOnce since this is not a explicit close and there might be 325 // more legitimate DB operations coming in 326 l.dbOpenerOnce = new(sync.Once) 327 return fn, err 328 } 329 330 func (l *LevelDb) nukeIfCorrupt(err error) bool { 331 if l.isCorrupt(err) { 332 l.G().Log.Debug("LevelDB file corrupted, nuking database and starting fresh") 333 if _, err := l.Nuke(); err != nil { 334 l.G().Log.Debug("Error nuking LevelDB file: %s", err) 335 return false 336 } 337 return true 338 } 339 return false 340 } 341 342 func (l *LevelDb) Put(id DbKey, aliases []DbKey, value []byte) error { 343 return l.doWhileOpenAndNukeIfCorrupted(func() error { 344 return levelDbPut(l.db, l.cleaner, id, aliases, value) 345 }) 346 } 347 348 func (l *LevelDb) Get(id DbKey) (val []byte, found bool, err error) { 349 err = l.doWhileOpenAndNukeIfCorrupted(func() error { 350 val, found, err = levelDbGet(l.db, l.cleaner, id) 351 return err 352 }) 353 return val, found, err 354 } 355 356 func (l *LevelDb) Lookup(id DbKey) (val []byte, found bool, err error) { 357 err = l.doWhileOpenAndNukeIfCorrupted(func() error { 358 val, found, err = levelDbLookup(l.db, l.cleaner, id) 359 return err 360 }) 361 return val, found, err 362 } 363 364 func (l *LevelDb) Delete(id DbKey) error { 365 return l.doWhileOpenAndNukeIfCorrupted(func() error { 366 return levelDbDelete(l.db, l.cleaner, id) 367 }) 368 } 369 370 func (l *LevelDb) OpenTransaction() (LocalDbTransaction, error) { 371 var ( 372 ltr LevelDbTransaction 373 err error 374 ) 375 if ltr.tr, err = l.db.OpenTransaction(); err != nil { 376 return LevelDbTransaction{}, err 377 } 378 ltr.cleaner = l.cleaner 379 return ltr, nil 380 } 381 382 func (l *LevelDb) KeysWithPrefixes(prefixes ...[]byte) (DBKeySet, error) { 383 m := make(map[DbKey]struct{}) 384 err := l.doWhileOpenAndNukeIfCorrupted(func() error { 385 opts := &opt.ReadOptions{DontFillCache: true} 386 for _, prefix := range prefixes { 387 iter := l.db.NewIterator(util.BytesPrefix(prefix), opts) 388 for iter.Next() { 389 _, dbKey, err := DbKeyParse(string(iter.Key())) 390 if err != nil { 391 iter.Release() 392 return err 393 } 394 m[dbKey] = struct{}{} 395 } 396 iter.Release() 397 err := iter.Error() 398 if err != nil { 399 return nil 400 } 401 } 402 return nil 403 }) 404 if err != nil { 405 return nil, err 406 } 407 408 return m, nil 409 } 410 411 type LevelDbTransaction struct { 412 tr *leveldb.Transaction 413 cleaner *levelDbCleaner 414 } 415 416 func (l LevelDbTransaction) Put(id DbKey, aliases []DbKey, value []byte) error { 417 return levelDbPut(l.tr, l.cleaner, id, aliases, value) 418 } 419 420 func (l LevelDbTransaction) Get(id DbKey) (val []byte, found bool, err error) { 421 return levelDbGet(l.tr, l.cleaner, id) 422 } 423 424 func (l LevelDbTransaction) Lookup(id DbKey) (val []byte, found bool, err error) { 425 return levelDbLookup(l.tr, l.cleaner, id) 426 } 427 428 func (l LevelDbTransaction) Delete(id DbKey) error { 429 return levelDbDelete(l.tr, l.cleaner, id) 430 } 431 432 func (l LevelDbTransaction) Commit() (err error) { 433 defer convertNoSpaceError(&err) 434 return l.tr.Commit() 435 } 436 437 func (l LevelDbTransaction) Discard() { 438 l.tr.Discard() 439 } 440 441 func convertNoSpaceError(err *error) { 442 if IsNoSpaceOnDeviceError(*err) { 443 // embed in exportable error type 444 *err = NoSpaceOnDeviceError{Desc: (*err).Error()} 445 } 446 }