lib/db: Refactor: use a Lowlevel type underneath Instance (ref #5198) (#5212)

This adds a thin type that holds the state associated with the
leveldb.DB, leaving the huge Instance type more or less stateless. Also
moves some keying stuff into the DB package so that other packages need
not know the keying specifics.

(This does not, yet, fix the cmd/stindex program, in order to keep the
diff size down. Hence the keying constants are still exported.)
This commit is contained in:
Jakob Borg 2018-10-10 11:34:24 +02:00 committed by GitHub
parent 8e645ab782
commit b50d57b7fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 382 additions and 374 deletions

View File

@ -16,7 +16,7 @@ import (
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
func dump(ldb *db.Instance) { func dump(ldb *db.Lowlevel) {
it := ldb.NewIterator(nil, nil) it := ldb.NewIterator(nil, nil)
for it.Next() { for it.Next() {
key := it.Key() key := it.Key()

View File

@ -37,7 +37,7 @@ func (h *ElementHeap) Pop() interface{} {
return x return x
} }
func dumpsize(ldb *db.Instance) { func dumpsize(ldb *db.Lowlevel) {
h := &ElementHeap{} h := &ElementHeap{}
heap.Init(h) heap.Init(h)

View File

@ -712,11 +712,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
if err != nil { if err != nil {
l.Fatalln("Error opening database:", err) l.Fatalln("Error opening database:", err)
} }
if err := db.UpdateSchema(ldb); err != nil {
l.Fatalln("Database schema:", err)
}
if runtimeOptions.resetDeltaIdxs { if runtimeOptions.resetDeltaIdxs {
l.Infoln("Reinitializing delta index IDs") l.Infoln("Reinitializing delta index IDs")
ldb.DropLocalDeltaIndexIDs() db.DropDeltaIndexIDs(ldb)
ldb.DropRemoteDeltaIndexIDs()
} }
protectedFiles := []string{ protectedFiles := []string{
@ -737,7 +739,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Grab the previously running version string from the database. // Grab the previously running version string from the database.
miscDB := db.NewNamespacedKV(ldb, string(db.KeyTypeMiscData)) miscDB := db.NewMiscDataNamespace(ldb)
prevVersion, _ := miscDB.String("prevVersion") prevVersion, _ := miscDB.String("prevVersion")
// Strip away prerelease/beta stuff and just compare the release // Strip away prerelease/beta stuff and just compare the release
@ -753,7 +755,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
// Drop delta indexes in case we've changed random stuff we // Drop delta indexes in case we've changed random stuff we
// shouldn't have. We will resend our index on next connect. // shouldn't have. We will resend our index on next connect.
ldb.DropLocalDeltaIndexIDs() db.DropDeltaIndexIDs(ldb)
// Remember the new version. // Remember the new version.
miscDB.PutString("prevVersion", Version) miscDB.PutString("prevVersion", Version)

View File

@ -45,7 +45,7 @@ func lazyInitBenchFileSet() {
replace(benchS, protocol.LocalDeviceID, firstHalf) replace(benchS, protocol.LocalDeviceID, firstHalf)
} }
func tempDB() (*db.Instance, string) { func tempDB() (*db.Lowlevel, string) {
dir, err := ioutil.TempDir("", "syncthing") dir, err := ioutil.TempDir("", "syncthing")
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -22,14 +22,14 @@ var blockFinder *BlockFinder
const maxBatchSize = 1000 const maxBatchSize = 1000
type BlockMap struct { type BlockMap struct {
db *Instance db *Lowlevel
folder uint32 folder uint32
} }
func NewBlockMap(db *Instance, folder uint32) *BlockMap { func NewBlockMap(db *Lowlevel, folder string) *BlockMap {
return &BlockMap{ return &BlockMap{
db: db, db: db,
folder: folder, folder: db.folderIdx.ID([]byte(folder)),
} }
} }
@ -139,10 +139,10 @@ func (m *BlockMap) blockKeyInto(o, hash []byte, file string) []byte {
} }
type BlockFinder struct { type BlockFinder struct {
db *Instance db *Lowlevel
} }
func NewBlockFinder(db *Instance) *BlockFinder { func NewBlockFinder(db *Lowlevel) *BlockFinder {
if blockFinder != nil { if blockFinder != nil {
return blockFinder return blockFinder
} }

View File

@ -48,14 +48,14 @@ func init() {
} }
} }
func setup() (*Instance, *BlockFinder) { func setup() (*Lowlevel, *BlockFinder) {
// Setup // Setup
db := OpenMemory() db := OpenMemory()
return db, NewBlockFinder(db) return db, NewBlockFinder(db)
} }
func dbEmpty(db *Instance) bool { func dbEmpty(db *Lowlevel) bool {
iter := db.NewIterator(util.BytesPrefix([]byte{KeyTypeBlock}), nil) iter := db.NewIterator(util.BytesPrefix([]byte{KeyTypeBlock}), nil)
defer iter.Release() defer iter.Release()
return !iter.Next() return !iter.Next()
@ -68,7 +68,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
t.Fatal("db not empty") t.Fatal("db not empty")
} }
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1"))) m := NewBlockMap(db, "folder1")
f3.Type = protocol.FileInfoTypeDirectory f3.Type = protocol.FileInfoTypeDirectory
@ -152,8 +152,8 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
func TestBlockFinderLookup(t *testing.T) { func TestBlockFinderLookup(t *testing.T) {
db, f := setup() db, f := setup()
m1 := NewBlockMap(db, db.folderIdx.ID([]byte("folder1"))) m1 := NewBlockMap(db, "folder1")
m2 := NewBlockMap(db, db.folderIdx.ID([]byte("folder2"))) m2 := NewBlockMap(db, "folder2")
err := m1.Add([]protocol.FileInfo{f1}) err := m1.Add([]protocol.FileInfo{f1})
if err != nil { if err != nil {

View File

@ -16,7 +16,7 @@ func TestDeviceKey(t *testing.T) {
dev := []byte("device67890123456789012345678901") dev := []byte("device67890123456789012345678901")
name := []byte("name") name := []byte("name")
db := OpenMemory() db := newInstance(OpenMemory())
key := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name) key := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name)
@ -44,7 +44,7 @@ func TestGlobalKey(t *testing.T) {
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123") fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
name := []byte("name") name := []byte("name")
db := OpenMemory() db := newInstance(OpenMemory())
key := db.keyer.GenerateGlobalVersionKey(nil, fld, name) key := db.keyer.GenerateGlobalVersionKey(nil, fld, name)
@ -69,7 +69,7 @@ func TestGlobalKey(t *testing.T) {
func TestSequenceKey(t *testing.T) { func TestSequenceKey(t *testing.T) {
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123") fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
db := OpenMemory() db := newInstance(OpenMemory())
const seq = 1234567890 const seq = 1234567890
key := db.keyer.GenerateSequenceKey(nil, fld, seq) key := db.keyer.GenerateSequenceKey(nil, fld, seq)

View File

@ -31,7 +31,7 @@ func (vl VersionList) String() string {
// update brings the VersionList up to date with file. It returns the updated // update brings the VersionList up to date with file. It returns the updated
// VersionList, a potentially removed old FileVersion and its index, as well as // VersionList, a potentially removed old FileVersion and its index, as well as
// the index where the new FileVersion was inserted. // the index where the new FileVersion was inserted.
func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *Instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) { func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) {
removedAt, insertedAt = -1, -1 removedAt, insertedAt = -1, -1
for i, v := range vl.Versions { for i, v := range vl.Versions {
if bytes.Equal(v.Device, device) { if bytes.Equal(v.Device, device) {

View File

@ -10,87 +10,28 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"os"
"sort"
"strings"
"sync/atomic"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/iterator" "github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
"github.com/syndtr/goleveldb/leveldb/util" "github.com/syndtr/goleveldb/leveldb/util"
) )
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator) type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator)
type Instance struct { type instance struct {
committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms *Lowlevel
*leveldb.DB
location string
folderIdx *smallIndex
deviceIdx *smallIndex
keyer keyer keyer keyer
} }
func Open(file string) (*Instance, error) { func newInstance(ll *Lowlevel) *instance {
opts := &opt.Options{ return &instance{
OpenFilesCacheCapacity: 100, Lowlevel: ll,
WriteBuffer: 4 << 20, keyer: newDefaultKeyer(ll.folderIdx, ll.deviceIdx),
} }
db, err := leveldb.OpenFile(file, opts)
if leveldbIsCorrupted(err) {
db, err = leveldb.RecoverFile(file, opts)
}
if leveldbIsCorrupted(err) {
// The database is corrupted, and we've tried to recover it but it
// didn't work. At this point there isn't much to do beyond dropping
// the database and reindexing...
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
if err := os.RemoveAll(file); err != nil {
return nil, errorSuggestion{err, "failed to delete corrupted database"}
}
db, err = leveldb.OpenFile(file, opts)
}
if err != nil {
return nil, errorSuggestion{err, "is another instance of Syncthing running?"}
}
return newDBInstance(db, file)
} }
func OpenMemory() *Instance { func (db *instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
ldb, _ := newDBInstance(db, "<memory>")
return ldb
}
func newDBInstance(db *leveldb.DB, location string) (*Instance, error) {
i := &Instance{
DB: db,
location: location,
folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}),
deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}),
}
i.keyer = newDefaultKeyer(i.folderIdx, i.deviceIdx)
err := i.updateSchema()
return i, err
}
// Committed returns the number of items committed to the database since startup
func (db *Instance) Committed() int64 {
return atomic.LoadInt64(&db.committed)
}
// Location returns the filesystem path where the database is stored
func (db *Instance) Location() string {
return db.location
}
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -131,7 +72,7 @@ func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, m
} }
} }
func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) { func (db *instance) addSequences(folder []byte, fs []protocol.FileInfo) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -146,7 +87,7 @@ func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) {
} }
} }
func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) { func (db *instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -158,7 +99,7 @@ func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
} }
} }
func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) { func (db *instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
if len(prefix) > 0 { if len(prefix) > 0 {
unslashedPrefix := prefix unslashedPrefix := prefix
if bytes.HasSuffix(prefix, []byte{'/'}) { if bytes.HasSuffix(prefix, []byte{'/'}) {
@ -199,7 +140,7 @@ func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn It
} }
} }
func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) { func (db *instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) {
t := db.newReadOnlyTransaction() t := db.newReadOnlyTransaction()
defer t.close() defer t.close()
@ -226,7 +167,7 @@ func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator)
} }
} }
func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) { func (db *instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -271,14 +212,14 @@ func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte,
} }
} }
func (db *Instance) getFile(key []byte) (protocol.FileInfo, bool) { func (db *instance) getFile(key []byte) (protocol.FileInfo, bool) {
if f, ok := db.getFileTrunc(key, false); ok { if f, ok := db.getFileTrunc(key, false); ok {
return f.(protocol.FileInfo), true return f.(protocol.FileInfo), true
} }
return protocol.FileInfo{}, false return protocol.FileInfo{}, false
} }
func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) { func (db *instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
bs, err := db.Get(key, nil) bs, err := db.Get(key, nil)
if err == leveldb.ErrNotFound { if err == leveldb.ErrNotFound {
return nil, false return nil, false
@ -296,7 +237,7 @@ func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
return f, true return f, true
} }
func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) { func (db *instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
t := db.newReadOnlyTransaction() t := db.newReadOnlyTransaction()
defer t.close() defer t.close()
@ -304,7 +245,7 @@ func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, boo
return f, ok return f, ok
} }
func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) { func (db *instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) {
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, file) gk = db.keyer.GenerateGlobalVersionKey(gk, folder, file)
bs, err := t.Get(gk, nil) bs, err := t.Get(gk, nil)
@ -325,7 +266,7 @@ func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []
return gk, dk, nil, false return gk, dk, nil, false
} }
func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) { func (db *instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) {
if len(prefix) > 0 { if len(prefix) > 0 {
unslashedPrefix := prefix unslashedPrefix := prefix
if bytes.HasSuffix(prefix, []byte{'/'}) { if bytes.HasSuffix(prefix, []byte{'/'}) {
@ -370,7 +311,7 @@ func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator
} }
} }
func (db *Instance) availability(folder, file []byte) []protocol.DeviceID { func (db *instance) availability(folder, file []byte) []protocol.DeviceID {
k := db.keyer.GenerateGlobalVersionKey(nil, folder, file) k := db.keyer.GenerateGlobalVersionKey(nil, folder, file)
bs, err := db.Get(k, nil) bs, err := db.Get(k, nil)
if err == leveldb.ErrNotFound { if err == leveldb.ErrNotFound {
@ -401,7 +342,7 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
return devices return devices
} }
func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) { func (db *instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
if bytes.Equal(device, protocol.LocalDeviceID[:]) { if bytes.Equal(device, protocol.LocalDeviceID[:]) {
db.withNeedLocal(folder, truncate, fn) db.withNeedLocal(folder, truncate, fn)
return return
@ -473,7 +414,7 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator)
} }
} }
func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) { func (db *instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
t := db.newReadOnlyTransaction() t := db.newReadOnlyTransaction()
defer t.close() defer t.close()
@ -495,31 +436,7 @@ func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
} }
} }
func (db *Instance) ListFolders() []string { func (db *instance) dropFolder(folder []byte) {
t := db.newReadOnlyTransaction()
defer t.close()
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
defer dbi.Release()
folderExists := make(map[string]bool)
for dbi.Next() {
folder, ok := db.keyer.FolderFromGlobalVersionKey(dbi.Key())
if ok && !folderExists[string(folder)] {
folderExists[string(folder)] = true
}
}
folders := make([]string, 0, len(folderExists))
for k := range folderExists {
folders = append(folders, k)
}
sort.Strings(folders)
return folders
}
func (db *Instance) dropFolder(folder []byte) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -537,7 +454,7 @@ func (db *Instance) dropFolder(folder []byte) {
} }
} }
func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) { func (db *instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -556,7 +473,7 @@ func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracke
} }
} }
func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) { func (db *instance) checkGlobals(folder []byte, meta *metadataTracker) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -604,7 +521,7 @@ func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
l.Debugf("db check completed for %q", folder) l.Debugf("db check completed for %q", folder)
} }
func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID { func (db *instance) getIndexID(device, folder []byte) protocol.IndexID {
key := db.keyer.GenerateIndexIDKey(nil, device, folder) key := db.keyer.GenerateIndexIDKey(nil, device, folder)
cur, err := db.Get(key, nil) cur, err := db.Get(key, nil)
if err != nil { if err != nil {
@ -619,7 +536,7 @@ func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
return id return id
} }
func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) { func (db *instance) setIndexID(device, folder []byte, id protocol.IndexID) {
key := db.keyer.GenerateIndexIDKey(nil, device, folder) key := db.keyer.GenerateIndexIDKey(nil, device, folder)
bs, _ := id.Marshal() // marshalling can't fail bs, _ := id.Marshal() // marshalling can't fail
if err := db.Put(key, bs, nil); err != nil { if err := db.Put(key, bs, nil); err != nil {
@ -627,44 +544,15 @@ func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
} }
} }
// DropLocalDeltaIndexIDs removes all index IDs for the local device ID from func (db *instance) dropMtimes(folder []byte) {
// the database. This will cause a full index transmission on the next
// connection.
func (db *Instance) DropLocalDeltaIndexIDs() {
db.dropDeltaIndexIDs(true)
}
// DropRemoteDeltaIndexIDs removes all index IDs for the other devices than
// the local one from the database. This will cause them to send us a full
// index on the next connection.
func (db *Instance) DropRemoteDeltaIndexIDs() {
db.dropDeltaIndexIDs(false)
}
func (db *Instance) dropDeltaIndexIDs(local bool) {
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
defer dbi.Release()
for dbi.Next() {
device, _ := db.keyer.DeviceFromIndexIDKey(dbi.Key())
if bytes.Equal(device, protocol.LocalDeviceID[:]) == local {
t.Delete(dbi.Key())
}
}
}
func (db *Instance) dropMtimes(folder []byte) {
db.dropPrefix(db.keyer.GenerateMtimesKey(nil, folder)) db.dropPrefix(db.keyer.GenerateMtimesKey(nil, folder))
} }
func (db *Instance) dropFolderMeta(folder []byte) { func (db *instance) dropFolderMeta(folder []byte) {
db.dropPrefix(db.keyer.GenerateFolderMetaKey(nil, folder)) db.dropPrefix(db.keyer.GenerateFolderMetaKey(nil, folder))
} }
func (db *Instance) dropPrefix(prefix []byte) { func (db *instance) dropPrefix(prefix []byte) {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -701,22 +589,6 @@ func unmarshalVersionList(data []byte) (VersionList, bool) {
return vl, true return vl, true
} }
// A "better" version of leveldb's errors.IsCorrupted.
func leveldbIsCorrupted(err error) bool {
switch {
case err == nil:
return false
case errors.IsCorrupted(err):
return true
case strings.Contains(err.Error(), "corrupted"):
return true
}
return false
}
type errorSuggestion struct { type errorSuggestion struct {
inner error inner error
suggestion string suggestion string

View File

@ -38,8 +38,17 @@ func (e databaseDowngradeError) Error() string {
return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion) return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion)
} }
func (db *Instance) updateSchema() error { func UpdateSchema(ll *Lowlevel) error {
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData)) updater := &schemaUpdater{newInstance(ll)}
return updater.updateSchema()
}
type schemaUpdater struct {
*instance
}
func (db *schemaUpdater) updateSchema() error {
miscDB := NewMiscDataNamespace(db.Lowlevel)
prevVersion, _ := miscDB.Int64("dbVersion") prevVersion, _ := miscDB.Int64("dbVersion")
if prevVersion > dbVersion { if prevVersion > dbVersion {
@ -77,7 +86,7 @@ func (db *Instance) updateSchema() error {
return nil return nil
} }
func (db *Instance) updateSchema0to1() { func (db *schemaUpdater) updateSchema0to1() {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -159,7 +168,7 @@ func (db *Instance) updateSchema0to1() {
// updateSchema1to2 introduces a sequenceKey->deviceKey bucket for local items // updateSchema1to2 introduces a sequenceKey->deviceKey bucket for local items
// to allow iteration in sequence order (simplifies sending indexes). // to allow iteration in sequence order (simplifies sending indexes).
func (db *Instance) updateSchema1to2() { func (db *schemaUpdater) updateSchema1to2() {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -178,7 +187,7 @@ func (db *Instance) updateSchema1to2() {
} }
// updateSchema2to3 introduces a needKey->nil bucket for locally needed files. // updateSchema2to3 introduces a needKey->nil bucket for locally needed files.
func (db *Instance) updateSchema2to3() { func (db *schemaUpdater) updateSchema2to3() {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
defer t.close() defer t.close()
@ -209,7 +218,7 @@ func (db *Instance) updateSchema2to3() {
// release candidates (dbVersion 3 and 4) // release candidates (dbVersion 3 and 4)
// https://github.com/syncthing/syncthing/issues/5007 // https://github.com/syncthing/syncthing/issues/5007
// https://github.com/syncthing/syncthing/issues/5053 // https://github.com/syncthing/syncthing/issues/5053
func (db *Instance) updateSchemaTo5() { func (db *schemaUpdater) updateSchemaTo5() {
t := db.newReadWriteTransaction() t := db.newReadWriteTransaction()
var nk []byte var nk []byte
for _, folderStr := range db.ListFolders() { for _, folderStr := range db.ListFolders() {
@ -221,7 +230,7 @@ func (db *Instance) updateSchemaTo5() {
db.updateSchema2to3() db.updateSchema2to3()
} }
func (db *Instance) updateSchema5to6() { func (db *schemaUpdater) updateSchema5to6() {
// For every local file with the Invalid bit set, clear the Invalid bit and // For every local file with the Invalid bit set, clear the Invalid bit and
// set LocalFlags = FlagLocalIgnored. // set LocalFlags = FlagLocalIgnored.

View File

@ -7,107 +7,20 @@
package db package db
import ( import (
"os"
"testing" "testing"
"github.com/syncthing/syncthing/lib/fs" "github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
func TestDropIndexIDs(t *testing.T) {
db := OpenMemory()
d1 := []byte("device67890123456789012345678901")
d2 := []byte("device12345678901234567890123456")
// Set some index IDs
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
db.setIndexID(d1, []byte("foo"), 3)
db.setIndexID(d1, []byte("bar"), 4)
db.setIndexID(d2, []byte("foo"), 5)
db.setIndexID(d2, []byte("bar"), 6)
// Verify them
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
t.Fatal("fail local 1")
}
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
t.Fatal("fail local 2")
}
if db.getIndexID(d1, []byte("foo")) != 3 {
t.Fatal("fail remote 1")
}
if db.getIndexID(d1, []byte("bar")) != 4 {
t.Fatal("fail remote 2")
}
if db.getIndexID(d2, []byte("foo")) != 5 {
t.Fatal("fail remote 3")
}
if db.getIndexID(d2, []byte("bar")) != 6 {
t.Fatal("fail remote 4")
}
// Drop the local ones, verify only they got dropped
db.DropLocalDeltaIndexIDs()
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 0 {
t.Fatal("fail local 1")
}
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 0 {
t.Fatal("fail local 2")
}
if db.getIndexID(d1, []byte("foo")) != 3 {
t.Fatal("fail remote 1")
}
if db.getIndexID(d1, []byte("bar")) != 4 {
t.Fatal("fail remote 2")
}
if db.getIndexID(d2, []byte("foo")) != 5 {
t.Fatal("fail remote 3")
}
if db.getIndexID(d2, []byte("bar")) != 6 {
t.Fatal("fail remote 4")
}
// Set local ones again
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
// Drop the remote ones, verify only they got dropped
db.DropRemoteDeltaIndexIDs()
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
t.Fatal("fail local 1")
}
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
t.Fatal("fail local 2")
}
if db.getIndexID(d1, []byte("foo")) != 0 {
t.Fatal("fail remote 1")
}
if db.getIndexID(d1, []byte("bar")) != 0 {
t.Fatal("fail remote 2")
}
if db.getIndexID(d2, []byte("foo")) != 0 {
t.Fatal("fail remote 3")
}
if db.getIndexID(d2, []byte("bar")) != 0 {
t.Fatal("fail remote 4")
}
}
func TestIgnoredFiles(t *testing.T) { func TestIgnoredFiles(t *testing.T) {
ldb, err := openJSONS("testdata/v0.14.48-ignoredfiles.db.jsons") ldb, err := openJSONS("testdata/v0.14.48-ignoredfiles.db.jsons")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
db, _ := newDBInstance(ldb, "<memory>") db := NewLowlevel(ldb, "<memory>")
UpdateSchema(db)
fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db) fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
// The contents of the database are like this: // The contents of the database are like this:
@ -228,11 +141,13 @@ func TestUpdate0to3(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
db, _ := newDBInstance(ldb, "<memory>")
db := newInstance(NewLowlevel(ldb, "<memory>"))
updater := schemaUpdater{db}
folder := []byte(update0to3Folder) folder := []byte(update0to3Folder)
db.updateSchema0to1() updater.updateSchema0to1()
if _, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], []byte(slashPrefixed))); ok { if _, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], []byte(slashPrefixed))); ok {
t.Error("File prefixed by '/' was not removed during transition to schema 1") t.Error("File prefixed by '/' was not removed during transition to schema 1")
@ -242,7 +157,7 @@ func TestUpdate0to3(t *testing.T) {
t.Error("Invalid file wasn't added to global list") t.Error("Invalid file wasn't added to global list")
} }
db.updateSchema1to2() updater.updateSchema1to2()
found := false found := false
db.withHaveSequence(folder, 0, func(fi FileIntf) bool { db.withHaveSequence(folder, 0, func(fi FileIntf) bool {
@ -263,7 +178,7 @@ func TestUpdate0to3(t *testing.T) {
t.Error("Local file wasn't added to sequence bucket", err) t.Error("Local file wasn't added to sequence bucket", err)
} }
db.updateSchema2to3() updater.updateSchema2to3()
need := map[string]protocol.FileInfo{ need := map[string]protocol.FileInfo{
haveUpdate0to3[remoteDevice0][0].Name: haveUpdate0to3[remoteDevice0][0], haveUpdate0to3[remoteDevice0][0].Name: haveUpdate0to3[remoteDevice0][0],
@ -288,22 +203,17 @@ func TestUpdate0to3(t *testing.T) {
} }
func TestDowngrade(t *testing.T) { func TestDowngrade(t *testing.T) {
loc := "testdata/downgrade.db" db := OpenMemory()
db, err := Open(loc) UpdateSchema(db) // sets the min version etc
if err != nil {
t.Fatal(err)
}
defer func() {
db.Close()
os.RemoveAll(loc)
}()
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData)) // Bump the database version to something newer than we actually support
miscDB := NewMiscDataNamespace(db)
miscDB.PutInt64("dbVersion", dbVersion+1) miscDB.PutInt64("dbVersion", dbVersion+1)
l.Infoln(dbVersion) l.Infoln(dbVersion)
db.Close() // Pretend we just opened the DB and attempt to update it again
db, err = Open(loc) err := UpdateSchema(db)
if err, ok := err.(databaseDowngradeError); !ok { if err, ok := err.(databaseDowngradeError); !ok {
t.Fatal("Expected error due to database downgrade, got", err) t.Fatal("Expected error due to database downgrade, got", err)
} else if err.minSyncthingVersion != dbMinSyncthingVersion { } else if err.minSyncthingVersion != dbMinSyncthingVersion {

View File

@ -8,7 +8,6 @@ package db
import ( import (
"bytes" "bytes"
"sync/atomic"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
@ -18,10 +17,10 @@ import (
// A readOnlyTransaction represents a database snapshot. // A readOnlyTransaction represents a database snapshot.
type readOnlyTransaction struct { type readOnlyTransaction struct {
*leveldb.Snapshot *leveldb.Snapshot
db *Instance db *instance
} }
func (db *Instance) newReadOnlyTransaction() readOnlyTransaction { func (db *instance) newReadOnlyTransaction() readOnlyTransaction {
snap, err := db.GetSnapshot() snap, err := db.GetSnapshot()
if err != nil { if err != nil {
panic(err) panic(err)
@ -48,7 +47,7 @@ type readWriteTransaction struct {
*leveldb.Batch *leveldb.Batch
} }
func (db *Instance) newReadWriteTransaction() readWriteTransaction { func (db *instance) newReadWriteTransaction() readWriteTransaction {
t := db.newReadOnlyTransaction() t := db.newReadOnlyTransaction()
return readWriteTransaction{ return readWriteTransaction{
readOnlyTransaction: t, readOnlyTransaction: t,
@ -72,7 +71,6 @@ func (t readWriteTransaction) flush() {
if err := t.db.Write(t.Batch, nil); err != nil { if err := t.db.Write(t.Batch, nil); err != nil {
panic(err) panic(err)
} }
atomic.AddInt64(&t.db.committed, int64(t.Batch.Len()))
} }
func (t readWriteTransaction) insertFile(fk, folder, device []byte, file protocol.FileInfo) { func (t readWriteTransaction) insertFile(fk, folder, device []byte, file protocol.FileInfo) {

122
lib/db/lowlevel.go Normal file
View File

@ -0,0 +1,122 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package db
import (
"os"
"strings"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/errors"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
)
const (
dbMaxOpenFiles = 100
dbWriteBuffer = 4 << 20
)
// Lowlevel is the lowest level database interface. It has a very simple
// purpose: hold the actual *leveldb.DB database, and the in-memory state
// that belong to that database. In the same way that a single on disk
// database can only be opened once, there should be only one Lowlevel for
// any given *leveldb.DB.
type Lowlevel struct {
committed int64 // atomic, must come first
*leveldb.DB
location string
folderIdx *smallIndex
deviceIdx *smallIndex
}
// Open attempts to open the database at the given location, and runs
// recovery on it if opening fails. Worst case, if recovery is not possible,
// the database is erased and created from scratch.
func Open(location string) (*Lowlevel, error) {
opts := &opt.Options{
OpenFilesCacheCapacity: dbMaxOpenFiles,
WriteBuffer: dbWriteBuffer,
}
db, err := leveldb.OpenFile(location, opts)
if leveldbIsCorrupted(err) {
db, err = leveldb.RecoverFile(location, opts)
}
if leveldbIsCorrupted(err) {
// The database is corrupted, and we've tried to recover it but it
// didn't work. At this point there isn't much to do beyond dropping
// the database and reindexing...
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
if err := os.RemoveAll(location); err != nil {
return nil, errorSuggestion{err, "failed to delete corrupted database"}
}
db, err = leveldb.OpenFile(location, opts)
}
if err != nil {
return nil, errorSuggestion{err, "is another instance of Syncthing running?"}
}
return NewLowlevel(db, location), nil
}
// OpenMemory returns a new Lowlevel referencing an in-memory database.
func OpenMemory() *Lowlevel {
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
return NewLowlevel(db, "<memory>")
}
// Location returns the filesystem path where the database is stored
func (db *Lowlevel) Location() string {
return db.location
}
// ListFolders returns the list of folders currently in the database
func (db *Lowlevel) ListFolders() []string {
return db.folderIdx.Values()
}
// Committed returns the number of items committed to the database since startup
func (db *Lowlevel) Committed() int64 {
return atomic.LoadInt64(&db.committed)
}
func (db *Lowlevel) Put(key, val []byte, wo *opt.WriteOptions) error {
atomic.AddInt64(&db.committed, 1)
return db.DB.Put(key, val, wo)
}
func (db *Lowlevel) Delete(key []byte, wo *opt.WriteOptions) error {
atomic.AddInt64(&db.committed, 1)
return db.DB.Delete(key, wo)
}
// NewLowlevel wraps the given *leveldb.DB into a *lowlevel
func NewLowlevel(db *leveldb.DB, location string) *Lowlevel {
return &Lowlevel{
DB: db,
location: location,
folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}),
deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}),
}
}
// A "better" version of leveldb's errors.IsCorrupted.
func leveldbIsCorrupted(err error) bool {
switch {
case err == nil:
return false
case errors.IsCorrupted(err):
return true
case strings.Contains(err.Error(), "corrupted"):
return true
}
return false
}

View File

@ -20,6 +20,7 @@ type metadataTracker struct {
mut sync.RWMutex mut sync.RWMutex
counts CountsSet counts CountsSet
indexes map[metaKey]int // device ID + local flags -> index in counts indexes map[metaKey]int // device ID + local flags -> index in counts
dirty bool
} }
type metaKey struct { type metaKey struct {
@ -55,18 +56,31 @@ func (m *metadataTracker) Marshal() ([]byte, error) {
// toDB saves the marshalled metadataTracker to the given db, under the key // toDB saves the marshalled metadataTracker to the given db, under the key
// corresponding to the given folder // corresponding to the given folder
func (m *metadataTracker) toDB(db *Instance, folder []byte) error { func (m *metadataTracker) toDB(db *instance, folder []byte) error {
key := db.keyer.GenerateFolderMetaKey(nil, folder) key := db.keyer.GenerateFolderMetaKey(nil, folder)
m.mut.RLock()
defer m.mut.RUnlock()
if !m.dirty {
return nil
}
bs, err := m.Marshal() bs, err := m.Marshal()
if err != nil { if err != nil {
return err return err
} }
return db.Put(key, bs, nil) err = db.Put(key, bs, nil)
if err == nil {
m.dirty = false
}
return err
} }
// fromDB initializes the metadataTracker from the marshalled data found in // fromDB initializes the metadataTracker from the marshalled data found in
// the database under the key corresponding to the given folder // the database under the key corresponding to the given folder
func (m *metadataTracker) fromDB(db *Instance, folder []byte) error { func (m *metadataTracker) fromDB(db *instance, folder []byte) error {
key := db.keyer.GenerateFolderMetaKey(nil, folder) key := db.keyer.GenerateFolderMetaKey(nil, folder)
bs, err := db.Get(key, nil) bs, err := db.Get(key, nil)
if err != nil { if err != nil {
@ -99,6 +113,7 @@ func (m *metadataTracker) addFile(dev protocol.DeviceID, f FileIntf) {
} }
m.mut.Lock() m.mut.Lock()
m.dirty = true
if flags := f.FileLocalFlags(); flags == 0 { if flags := f.FileLocalFlags(); flags == 0 {
// Account regular files in the zero-flags bucket. // Account regular files in the zero-flags bucket.
@ -141,6 +156,7 @@ func (m *metadataTracker) removeFile(dev protocol.DeviceID, f FileIntf) {
} }
m.mut.Lock() m.mut.Lock()
m.dirty = true
if flags := f.FileLocalFlags(); flags == 0 { if flags := f.FileLocalFlags(); flags == 0 {
// Remove regular files from the zero-flags bucket // Remove regular files from the zero-flags bucket
@ -194,6 +210,7 @@ func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flags uint32,
// resetAll resets all metadata for the given device // resetAll resets all metadata for the given device
func (m *metadataTracker) resetAll(dev protocol.DeviceID) { func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
m.mut.Lock() m.mut.Lock()
m.dirty = true
for i, c := range m.counts.Counts { for i, c := range m.counts.Counts {
if bytes.Equal(c.DeviceID, dev[:]) { if bytes.Equal(c.DeviceID, dev[:]) {
m.counts.Counts[i] = Counts{ m.counts.Counts[i] = Counts{
@ -209,6 +226,7 @@ func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
// sequence number // sequence number
func (m *metadataTracker) resetCounts(dev protocol.DeviceID) { func (m *metadataTracker) resetCounts(dev protocol.DeviceID) {
m.mut.Lock() m.mut.Lock()
m.dirty = true
for i, c := range m.counts.Counts { for i, c := range m.counts.Counts {
if bytes.Equal(c.DeviceID, dev[:]) { if bytes.Equal(c.DeviceID, dev[:]) {
@ -285,6 +303,7 @@ func (m *metadataTracker) Created() time.Time {
func (m *metadataTracker) SetCreated() { func (m *metadataTracker) SetCreated() {
m.mut.Lock() m.mut.Lock()
m.counts.Created = time.Now().UnixNano() m.counts.Created = time.Now().UnixNano()
m.dirty = true
m.mut.Unlock() m.mut.Unlock()
} }

View File

@ -17,13 +17,13 @@ import (
// NamespacedKV is a simple key-value store using a specific namespace within // NamespacedKV is a simple key-value store using a specific namespace within
// a leveldb. // a leveldb.
type NamespacedKV struct { type NamespacedKV struct {
db *Instance db *Lowlevel
prefix []byte prefix []byte
} }
// NewNamespacedKV returns a new NamespacedKV that lives in the namespace // NewNamespacedKV returns a new NamespacedKV that lives in the namespace
// specified by the prefix. // specified by the prefix.
func NewNamespacedKV(db *Instance, prefix string) *NamespacedKV { func NewNamespacedKV(db *Lowlevel, prefix string) *NamespacedKV {
return &NamespacedKV{ return &NamespacedKV{
db: db, db: db,
prefix: []byte(prefix), prefix: []byte(prefix),
@ -157,3 +157,23 @@ func (n NamespacedKV) Delete(key string) {
keyBs := append(n.prefix, []byte(key)...) keyBs := append(n.prefix, []byte(key)...)
n.db.Delete(keyBs, nil) n.db.Delete(keyBs, nil)
} }
// Well known namespaces that can be instantiated without knowing the key
// details.
// NewDeviceStatisticsNamespace creates a KV namespace for device statistics
// for the given device.
func NewDeviceStatisticsNamespace(db *Lowlevel, device string) *NamespacedKV {
return NewNamespacedKV(db, string(KeyTypeDeviceStatistic)+device)
}
// NewFolderStatisticsNamespace creates a KV namespace for folder statistics
// for the given folder.
func NewFolderStatisticsNamespace(db *Lowlevel, folder string) *NamespacedKV {
return NewNamespacedKV(db, string(KeyTypeFolderStatistic)+folder)
}
// NewMiscDateNamespace creates a KV namespace for miscellaneous metadata.
func NewMiscDataNamespace(db *Lowlevel) *NamespacedKV {
return NewNamespacedKV(db, string(KeyTypeMiscData))
}

View File

@ -21,12 +21,13 @@ import (
"github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
"github.com/syndtr/goleveldb/leveldb/util"
) )
type FileSet struct { type FileSet struct {
folder string folder string
fs fs.Filesystem fs fs.Filesystem
db *Instance db *instance
blockmap *BlockMap blockmap *BlockMap
meta *metadataTracker meta *metadataTracker
@ -66,12 +67,14 @@ func init() {
} }
} }
func NewFileSet(folder string, fs fs.Filesystem, db *Instance) *FileSet { func NewFileSet(folder string, fs fs.Filesystem, ll *Lowlevel) *FileSet {
db := newInstance(ll)
var s = FileSet{ var s = FileSet{
folder: folder, folder: folder,
fs: fs, fs: fs,
db: db, db: db,
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))), blockmap: NewBlockMap(ll, folder),
meta: newMetadataTracker(), meta: newMetadataTracker(),
updateMutex: sync.NewMutex(), updateMutex: sync.NewMutex(),
} }
@ -310,7 +313,7 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
func (s *FileSet) MtimeFS() *fs.MtimeFS { func (s *FileSet) MtimeFS() *fs.MtimeFS {
prefix := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder)) prefix := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
kv := NewNamespacedKV(s.db, string(prefix)) kv := NewNamespacedKV(s.db.Lowlevel, string(prefix))
return fs.NewMtimeFS(s.fs, kv) return fs.NewMtimeFS(s.fs, kv)
} }
@ -320,15 +323,26 @@ func (s *FileSet) ListDevices() []protocol.DeviceID {
// DropFolder clears out all information related to the given folder from the // DropFolder clears out all information related to the given folder from the
// database. // database.
func DropFolder(db *Instance, folder string) { func DropFolder(ll *Lowlevel, folder string) {
db := newInstance(ll)
db.dropFolder([]byte(folder)) db.dropFolder([]byte(folder))
db.dropMtimes([]byte(folder)) db.dropMtimes([]byte(folder))
db.dropFolderMeta([]byte(folder)) db.dropFolderMeta([]byte(folder))
bm := &BlockMap{ bm := NewBlockMap(ll, folder)
db: db,
folder: db.folderIdx.ID([]byte(folder)),
}
bm.Drop() bm.Drop()
// Also clean out the folder ID mapping.
db.folderIdx.Delete([]byte(folder))
}
// DropDeltaIndexIDs removes all delta index IDs from the database.
// This will cause a full index transmission on the next connection.
func DropDeltaIndexIDs(db *Lowlevel) {
dbi := db.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
defer dbi.Release()
for dbi.Next() {
db.Delete(dbi.Key(), nil)
}
} }
func normalizeFilenames(fs []protocol.FileInfo) { func normalizeFilenames(fs []protocol.FileInfo) {

View File

@ -8,6 +8,7 @@ package db
import ( import (
"encoding/binary" "encoding/binary"
"sort"
"github.com/syncthing/syncthing/lib/sync" "github.com/syncthing/syncthing/lib/sync"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
@ -46,8 +47,11 @@ func (i *smallIndex) load() {
for it.Next() { for it.Next() {
val := string(it.Value()) val := string(it.Value())
id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):]) id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):])
if val != "" {
// Empty value means the entry has been deleted.
i.id2val[id] = val i.id2val[id] = val
i.val2id[val] = id i.val2id[val] = id
}
if id >= i.nextID { if id >= i.nextID {
i.nextID = id + 1 i.nextID = id + 1
} }
@ -96,3 +100,45 @@ func (i *smallIndex) Val(id uint32) ([]byte, bool) {
return []byte(val), true return []byte(val), true
} }
func (i *smallIndex) Delete(val []byte) {
i.mut.Lock()
defer i.mut.Unlock()
// Check the reverse mapping to get the ID for the value.
if id, ok := i.val2id[string(val)]; ok {
// Generate the corresponding database key.
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
copy(key, i.prefix)
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
// Put an empty value into the database. This indicates that the
// entry does not exist any more and prevents the ID from being
// reused in the future.
i.db.Put(key, []byte{}, nil)
// Delete reverse mapping.
delete(i.id2val, id)
}
// Delete forward mapping.
delete(i.val2id, string(val))
}
// Values returns the set of values in the index
func (i *smallIndex) Values() []string {
// In principle this method should return [][]byte because all the other
// methods deal in []byte keys. However, in practice, where it's used
// wants a []string and it's easier to just create that here rather than
// having to convert both here and there...
i.mut.Lock()
vals := make([]string, 0, len(i.val2id))
for val := range i.val2id {
vals = append(vals, val)
}
i.mut.Unlock()
sort.Strings(vals)
return vals
}

52
lib/db/smallindex_test.go Normal file
View File

@ -0,0 +1,52 @@
// Copyright (C) 2018 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
package db
import "testing"
func TestSmallIndex(t *testing.T) {
db := OpenMemory()
idx := newSmallIndex(db.DB, []byte{12, 34})
// ID zero should be unallocated
if val, ok := idx.Val(0); ok || val != nil {
t.Fatal("Unexpected return for nonexistent ID 0")
}
// A new key should get ID zero
if id := idx.ID([]byte("hello")); id != 0 {
t.Fatal("Expected 0, not", id)
}
// Looking up ID zero should work
if val, ok := idx.Val(0); !ok || string(val) != "hello" {
t.Fatalf(`Expected true, "hello", not %v, %q`, ok, val)
}
// Delete the key
idx.Delete([]byte("hello"))
// Next ID should be one
if id := idx.ID([]byte("key2")); id != 1 {
t.Fatal("Expected 1, not", id)
}
// Now lets create a new index instance based on what's actually serialized to the database.
idx = newSmallIndex(db.DB, []byte{12, 34})
// Status should be about the same as before.
if val, ok := idx.Val(0); ok || val != nil {
t.Fatal("Unexpected return for deleted ID 0")
}
if id := idx.ID([]byte("key2")); id != 1 {
t.Fatal("Expected 1, not", id)
}
// Setting "hello" again should get us ID 2, not 0 as it was originally.
if id := idx.ID([]byte("hello")); id != 2 {
t.Fatal("Expected 2, not", id)
}
}

View File

@ -84,7 +84,7 @@ type Model struct {
*suture.Supervisor *suture.Supervisor
cfg *config.Wrapper cfg *config.Wrapper
db *db.Instance db *db.Lowlevel
finder *db.BlockFinder finder *db.BlockFinder
progressEmitter *ProgressEmitter progressEmitter *ProgressEmitter
id protocol.DeviceID id protocol.DeviceID
@ -134,7 +134,7 @@ var (
// NewModel creates and starts a new model. The model starts in read-only mode, // NewModel creates and starts a new model. The model starts in read-only mode,
// where it sends index information to connected peers and responds to requests // where it sends index information to connected peers and responds to requests
// for file data without altering the local folder in any way. // for file data without altering the local folder in any way.
func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Instance, protectedFiles []string) *Model { func NewModel(cfg *config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *Model {
m := &Model{ m := &Model{
Supervisor: suture.New("model", suture.Spec{ Supervisor: suture.New("model", suture.Spec{
Log: func(line string) { Log: func(line string) {

View File

@ -2618,60 +2618,6 @@ func TestIssue4357(t *testing.T) {
} }
} }
func TestScanNoDatabaseWrite(t *testing.T) {
// When scanning, nothing should be committed to database unless
// something actually changed.
db := db.OpenMemory()
m := NewModel(defaultCfgWrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
// Reach in and update the ignore matcher to one that always does
// reloads when asked to, instead of checking file mtimes. This is
// because we will be changing the files on disk often enough that the
// mtimes will be unreliable to determine change status.
m.fmut.Lock()
m.folderIgnores["default"] = ignore.New(defaultFs, ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
m.fmut.Unlock()
m.SetIgnores("default", nil)
defer os.Remove("testdata/.stignore")
// Scan the folder twice. The second scan should be a no-op database wise
m.ScanFolder("default")
c0 := db.Committed()
m.ScanFolder("default")
c1 := db.Committed()
if c1 != c0 {
t.Errorf("scan should not commit data when nothing changed but %d != %d", c1, c0)
}
// Ignore a file we know exists. It'll be updated in the database.
m.SetIgnores("default", []string{"foo"})
m.ScanFolder("default")
c2 := db.Committed()
if c2 <= c1 {
t.Errorf("scan should commit data when something got ignored but %d <= %d", c2, c1)
}
// Scan again. Nothing should happen.
m.ScanFolder("default")
c3 := db.Committed()
if c3 != c2 {
t.Errorf("scan should not commit data when nothing changed (with ignores) but %d != %d", c3, c2)
}
}
func TestIssue2782(t *testing.T) { func TestIssue2782(t *testing.T) {
// CheckHealth should accept a symlinked folder, when using tilde-expanded path. // CheckHealth should accept a symlinked folder, when using tilde-expanded path.

View File

@ -21,10 +21,9 @@ type DeviceStatisticsReference struct {
device string device string
} }
func NewDeviceStatisticsReference(ldb *db.Instance, device string) *DeviceStatisticsReference { func NewDeviceStatisticsReference(ldb *db.Lowlevel, device string) *DeviceStatisticsReference {
prefix := string(db.KeyTypeDeviceStatistic) + device
return &DeviceStatisticsReference{ return &DeviceStatisticsReference{
ns: db.NewNamespacedKV(ldb, prefix), ns: db.NewDeviceStatisticsNamespace(ldb, device),
device: device, device: device,
} }
} }

View File

@ -28,10 +28,9 @@ type LastFile struct {
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
} }
func NewFolderStatisticsReference(ldb *db.Instance, folder string) *FolderStatisticsReference { func NewFolderStatisticsReference(ldb *db.Lowlevel, folder string) *FolderStatisticsReference {
prefix := string(db.KeyTypeFolderStatistic) + folder
return &FolderStatisticsReference{ return &FolderStatisticsReference{
ns: db.NewNamespacedKV(ldb, prefix), ns: db.NewFolderStatisticsNamespace(ldb, folder),
folder: folder, folder: folder,
} }
} }