Determine conflict winner based on change type and modification time (fixes #1848)

This commit is contained in:
Jakob Borg 2015-07-21 15:29:02 +02:00
parent 6e4d33c741
commit da4ebb6535
5 changed files with 106 additions and 24 deletions

2
Godeps/Godeps.json generated
View File

@ -35,7 +35,7 @@
}, },
{ {
"ImportPath": "github.com/syncthing/protocol", "ImportPath": "github.com/syncthing/protocol",
"Rev": "7996ef0d45b7743ff930048b6413b37b2c33cd85" "Rev": "fcbd12b42176237d48985c1bfafbc43b2107aa31"
}, },
{ {
"ImportPath": "github.com/syndtr/goleveldb/leveldb", "ImportPath": "github.com/syndtr/goleveldb/leveldb",

View File

@ -0,0 +1,23 @@
// Copyright (C) 2015 The Protocol Authors.
package protocol
import "testing"
func TestWinsConflict(t *testing.T) {
testcases := [][2]FileInfo{
// The first should always win over the second
{{Modified: 42}, {Modified: 41}},
{{Modified: 41}, {Modified: 42, Flags: FlagDeleted}},
{{Modified: 41, Version: Vector{{42, 2}, {43, 1}}}, {Modified: 41, Version: Vector{{42, 1}, {43, 2}}}},
}
for _, tc := range testcases {
if !tc[0].WinsConflict(tc[1]) {
t.Errorf("%v should win over %v", tc[0], tc[1])
}
if tc[1].WinsConflict(tc[0]) {
t.Errorf("%v should not win over %v", tc[1], tc[0])
}
}
}

View File

@ -58,6 +58,31 @@ func (f FileInfo) HasPermissionBits() bool {
return f.Flags&FlagNoPermBits == 0 return f.Flags&FlagNoPermBits == 0
} }
// WinsConflict returns true if "f" is the one to choose when it is in
// conflict with "other".
func (f FileInfo) WinsConflict(other FileInfo) bool {
// If a modification is in conflict with a delete, we pick the
// modification.
if !f.IsDeleted() && other.IsDeleted() {
return true
}
if f.IsDeleted() && !other.IsDeleted() {
return false
}
// The one with the newer modification time wins.
if f.Modified > other.Modified {
return true
}
if f.Modified < other.Modified {
return false
}
// The modification times were equal. Use the device ID in the version
// vector as tie breaker.
return f.Version.Compare(other.Version) == ConcurrentGreater
}
type BlockInfo struct { type BlockInfo struct {
Offset int64 // noencode (cache only) Offset int64 // noencode (cache only)
Size int32 Size int32

View File

@ -236,7 +236,7 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
if fs[fsi].IsInvalid() { if fs[fsi].IsInvalid() {
ldbRemoveFromGlobal(snap, batch, folder, device, newName) ldbRemoveFromGlobal(snap, batch, folder, device, newName)
} else { } else {
ldbUpdateGlobal(snap, batch, folder, device, newName, fs[fsi].Version) ldbUpdateGlobal(snap, batch, folder, device, fs[fsi])
} }
fsi++ fsi++
@ -259,7 +259,7 @@ func ldbGenericReplace(db *leveldb.DB, folder, device []byte, fs []protocol.File
if fs[fsi].IsInvalid() { if fs[fsi].IsInvalid() {
ldbRemoveFromGlobal(snap, batch, folder, device, newName) ldbRemoveFromGlobal(snap, batch, folder, device, newName)
} else { } else {
ldbUpdateGlobal(snap, batch, folder, device, newName, fs[fsi].Version) ldbUpdateGlobal(snap, batch, folder, device, fs[fsi])
} }
} else if debugDB { } else if debugDB {
l.Debugln("generic replace; equal - ignore") l.Debugln("generic replace; equal - ignore")
@ -348,7 +348,7 @@ func ldbReplaceWithDelete(db *leveldb.DB, folder, device []byte, fs []protocol.F
} }
batch.Put(dbi.Key(), bs) batch.Put(dbi.Key(), bs)
mtimeRepo.DeleteMtime(tf.Name) mtimeRepo.DeleteMtime(tf.Name)
ldbUpdateGlobal(db, batch, folder, device, deviceKeyName(dbi.Key()), f.Version) ldbUpdateGlobal(db, batch, folder, device, f)
return ts return ts
} }
return 0 return 0
@ -392,7 +392,7 @@ func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) in
if f.IsInvalid() { if f.IsInvalid() {
ldbRemoveFromGlobal(snap, batch, folder, device, name) ldbRemoveFromGlobal(snap, batch, folder, device, name)
} else { } else {
ldbUpdateGlobal(snap, batch, folder, device, name, f.Version) ldbUpdateGlobal(snap, batch, folder, device, f)
} }
continue continue
} }
@ -411,7 +411,7 @@ func ldbUpdate(db *leveldb.DB, folder, device []byte, fs []protocol.FileInfo) in
if f.IsInvalid() { if f.IsInvalid() {
ldbRemoveFromGlobal(snap, batch, folder, device, name) ldbRemoveFromGlobal(snap, batch, folder, device, name)
} else { } else {
ldbUpdateGlobal(snap, batch, folder, device, name, f.Version) ldbUpdateGlobal(snap, batch, folder, device, f)
} }
} }
@ -464,11 +464,12 @@ func ldbInsert(batch dbWriter, folder, device []byte, file protocol.FileInfo) in
// ldbUpdateGlobal adds this device+version to the version list for the given // ldbUpdateGlobal adds this device+version to the version list for the given
// file. If the device is already present in the list, the version is updated. // file. If the device is already present in the list, the version is updated.
// If the file does not have an entry in the global list, it is created. // If the file does not have an entry in the global list, it is created.
func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device, file []byte, version protocol.Vector) bool { func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device []byte, file protocol.FileInfo) bool {
if debugDB { if debugDB {
l.Debugf("update global; folder=%q device=%v file=%q version=%d", folder, protocol.DeviceIDFromBytes(device), file, version) l.Debugf("update global; folder=%q device=%v file=%q version=%d", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version)
} }
gk := globalKey(folder, file) name := []byte(file.Name)
gk := globalKey(folder, name)
svl, err := db.Get(gk, nil) svl, err := db.Get(gk, nil)
if err != nil && err != leveldb.ErrNotFound { if err != nil && err != leveldb.ErrNotFound {
panic(err) panic(err)
@ -485,7 +486,7 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device, file []byte, v
for i := range fl.versions { for i := range fl.versions {
if bytes.Compare(fl.versions[i].device, device) == 0 { if bytes.Compare(fl.versions[i].device, device) == 0 {
if fl.versions[i].version.Equal(version) { if fl.versions[i].version.Equal(file.Version) {
// No need to do anything // No need to do anything
return false return false
} }
@ -497,21 +498,38 @@ func ldbUpdateGlobal(db dbReader, batch dbWriter, folder, device, file []byte, v
nv := fileVersion{ nv := fileVersion{
device: device, device: device,
version: version, version: file.Version,
} }
// Find a position in the list to insert this file. The file at the front
// of the list is the newer, the "global".
for i := range fl.versions { for i := range fl.versions {
// We compare against ConcurrentLesser as well here because we need switch fl.versions[i].version.Compare(file.Version) {
// to enforce a consistent ordering of versions even in the case of case protocol.Equal, protocol.Lesser:
// conflicts. // The version at this point in the list is equal to or lesser
if comp := fl.versions[i].version.Compare(version); comp == protocol.Equal || comp == protocol.Lesser || comp == protocol.ConcurrentLesser { // ("older") than us. We insert ourselves in front of it.
t := append(fl.versions, fileVersion{}) fl.versions = insertVersion(fl.versions, i, nv)
copy(t[i+1:], t[i:]) goto done
t[i] = nv
fl.versions = t case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
// The version at this point is in conflict with us. We must pull
// the actual file metadata to determine who wins. If we win, we
// insert ourselves in front of the loser here. (The "Lesser" and
// "Greater" in the condition above is just based on the device
// IDs in the version vector, which is not the only thing we use
// to determine the winner.)
of, ok := ldbGet(db, folder, fl.versions[i].device, name)
if !ok {
panic("file referenced in version list does not exist")
}
if file.WinsConflict(of) {
fl.versions = insertVersion(fl.versions, i, nv)
goto done goto done
} }
} }
}
// We didn't find a position for an insert above, so append to the end.
fl.versions = append(fl.versions, nv) fl.versions = append(fl.versions, nv)
done: done:
@ -524,6 +542,13 @@ done:
return true return true
} }
func insertVersion(vl []fileVersion, i int, v fileVersion) []fileVersion {
t := append(vl, fileVersion{})
copy(t[i+1:], t[i:])
t[i] = v
return t
}
// ldbRemoveFromGlobal removes the device from the global version list for the // ldbRemoveFromGlobal removes the device from the global version list for the
// given file. If the version list is empty after this, the file entry is // given file. If the version list is empty after this, the file entry is
// removed entirely. // removed entirely.
@ -644,7 +669,7 @@ func ldbWithAllFolderTruncated(db *leveldb.DB, folder []byte, fn func(device []b
} }
} }
func ldbGet(db *leveldb.DB, folder, device, file []byte) (protocol.FileInfo, bool) { func ldbGet(db dbReader, folder, device, file []byte) (protocol.FileInfo, bool) {
nk := deviceKey(folder, device, file) nk := deviceKey(folder, device, file)
bs, err := db.Get(nk, nil) bs, err := db.Get(nk, nil)
if err == leveldb.ErrNotFound { if err == leveldb.ErrNotFound {

View File

@ -9,6 +9,7 @@
package integration package integration
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -156,15 +157,23 @@ func TestConflictsDefault(t *testing.T) {
} }
rc.AwaitSync("default", sender, receiver) rc.AwaitSync("default", sender, receiver)
// The conflict should manifest on the s2 side again, where we should have // The conflict is resolved to the advantage of the edit over the delete.
// moved the file to a conflict copy instead of just deleting it. // As such, we get the edited content synced back to s1 where it was
// removed.
files, err = osutil.Glob("s2/*sync-conflict*") files, err = osutil.Glob("s2/*sync-conflict*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(files) != 2 { if len(files) != 1 {
t.Errorf("Expected 2 conflicted files instead of %d", len(files)) t.Errorf("Expected 1 conflicted files instead of %d", len(files))
}
bs, err := ioutil.ReadFile("s1/testfile.txt")
if err != nil {
t.Error("reading file:", err)
}
if !bytes.Contains(bs, []byte("more text added to s2")) {
t.Error("s1/testfile.txt should contain data added in s2")
} }
} }