mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-22 19:08:58 +00:00
Determine conflict winner based on change type and modification time (fixes #1848)
This commit is contained in:
parent
6e4d33c741
commit
da4ebb6535
2
Godeps/Godeps.json
generated
2
Godeps/Godeps.json
generated
@ -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",
|
||||||
|
23
Godeps/_workspace/src/github.com/syncthing/protocol/conflict_test.go
generated
vendored
Normal file
23
Godeps/_workspace/src/github.com/syncthing/protocol/conflict_test.go
generated
vendored
Normal 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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
Godeps/_workspace/src/github.com/syncthing/protocol/message.go
generated
vendored
25
Godeps/_workspace/src/github.com/syncthing/protocol/message.go
generated
vendored
@ -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
|
||||||
|
@ -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:])
|
|
||||||
t[i] = nv
|
|
||||||
fl.versions = t
|
|
||||||
goto done
|
goto done
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user