1
0
Fork 0
mirror of https://github.com/anyproto/any-sync.git synced 2025-06-09 09:35:03 +09:00

Tree builder fixes

This commit is contained in:
mcrakhman 2024-11-25 22:20:21 +01:00
parent 3c6284c750
commit c8a5002d8c
No known key found for this signature in database
GPG key ID: DED12CFEF5B8396B
3 changed files with 137 additions and 14 deletions

View file

@ -1091,6 +1091,109 @@ func TestObjectTree(t *testing.T) {
require.Equal(t, "0", objTree.Root().Id)
})
t.Run("find correct common snapshot", func(t *testing.T) {
// checking that adding old changes did not affect the tree
ctx := prepareTreeContext(t, aclList)
changeCreator := ctx.changeCreator
objTree := ctx.objTree
rawChanges := []*treechangeproto.RawTreeChangeWithId{
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
changeCreator.CreateRaw("2", aclList.Head().Id, "1", true, "1"),
changeCreator.CreateRaw("3", aclList.Head().Id, "2", true, "2"),
changeCreator.CreateRaw("4", aclList.Head().Id, "3", true, "3"),
changeCreator.CreateRaw("5", aclList.Head().Id, "4", true, "4"),
changeCreator.CreateRaw("6", aclList.Head().Id, "5", true, "5"),
changeCreator.CreateRaw("6.a.1", aclList.Head().Id, "6", true, "6"),
changeCreator.CreateRaw("6.a.2", aclList.Head().Id, "6.a.1", true, "6.a.1"),
changeCreator.CreateRaw("6.a.3", aclList.Head().Id, "6.a.2", true, "6.a.2"),
changeCreator.CreateRaw("6.b.1", aclList.Head().Id, "6", true, "6"),
changeCreator.CreateRaw("6.b.2", aclList.Head().Id, "6.b.1", true, "6.b.1"),
changeCreator.CreateRaw("6.b.3", aclList.Head().Id, "6.b.2", true, "6.b.2"),
}
payload := RawChangesPayload{
NewHeads: []string{"6.b.3", "6.a.3"},
RawChanges: rawChanges,
}
_, err := objTree.AddRawChanges(context.Background(), payload)
require.NoError(t, err, "adding changes should be without error")
require.Equal(t, "6", objTree.Root().Id)
rawChanges = []*treechangeproto.RawTreeChangeWithId{
changeCreator.CreateRaw("4.a.1", aclList.Head().Id, "4", true, "4"),
changeCreator.CreateRaw("4.a.2", aclList.Head().Id, "4.a.1", true, "4.a.1"),
changeCreator.CreateRaw("4.a.3", aclList.Head().Id, "4.a.2", true, "4.a.2"),
changeCreator.CreateRaw("4.b.1", aclList.Head().Id, "4", true, "4"),
changeCreator.CreateRaw("4.b.2", aclList.Head().Id, "4.b.1", true, "4.b.1"),
changeCreator.CreateRaw("4.b.3", aclList.Head().Id, "4.b.2", true, "4.b.2"),
changeCreator.CreateRaw("5.a.1", aclList.Head().Id, "5", true, "5"),
changeCreator.CreateRaw("5.a.2", aclList.Head().Id, "5.a.1", true, "5.a.1"),
changeCreator.CreateRaw("5.a.3", aclList.Head().Id, "5.a.2", true, "5.a.2"),
changeCreator.CreateRaw("5.b.1", aclList.Head().Id, "5", true, "5"),
changeCreator.CreateRaw("5.b.2", aclList.Head().Id, "5.b.1", true, "5.b.1"),
changeCreator.CreateRaw("5.b.3", aclList.Head().Id, "5.b.2", true, "5.b.2"),
}
payload = RawChangesPayload{
NewHeads: []string{"4.b.3", "4.a.3", "5.b.3", "5.a.3"},
RawChanges: rawChanges,
}
_, err = objTree.AddRawChanges(context.Background(), payload)
require.NoError(t, err, "adding changes should be without error")
require.Equal(t, "4", objTree.Root().Id)
})
t.Run("find correct common snapshot with existing path", func(t *testing.T) {
// checking that adding old changes did not affect the tree
ctx := prepareTreeContext(t, aclList)
changeCreator := ctx.changeCreator
objTree := ctx.objTree
rawChanges := []*treechangeproto.RawTreeChangeWithId{
changeCreator.CreateRaw("1", aclList.Head().Id, "0", true, "0"),
changeCreator.CreateRaw("2", aclList.Head().Id, "1", true, "1"),
changeCreator.CreateRaw("3", aclList.Head().Id, "2", true, "2"),
changeCreator.CreateRaw("4", aclList.Head().Id, "3", true, "3"),
changeCreator.CreateRaw("5", aclList.Head().Id, "4", true, "4"),
changeCreator.CreateRaw("6", aclList.Head().Id, "5", true, "5"),
changeCreator.CreateRaw("6.a.1", aclList.Head().Id, "6", true, "6"),
changeCreator.CreateRaw("6.a.2", aclList.Head().Id, "6.a.1", true, "6.a.1"),
changeCreator.CreateRaw("6.a.3", aclList.Head().Id, "6.a.2", true, "6.a.2"),
changeCreator.CreateRaw("6.b.1", aclList.Head().Id, "6", true, "6"),
changeCreator.CreateRaw("6.b.2", aclList.Head().Id, "6.b.1", true, "6.b.1"),
changeCreator.CreateRaw("6.b.3", aclList.Head().Id, "6.b.2", true, "6.b.2"),
}
payload := RawChangesPayload{
NewHeads: []string{"6.b.3", "6.a.3"},
RawChanges: rawChanges,
}
_, err := objTree.AddRawChanges(context.Background(), payload)
require.NoError(t, err, "adding changes should be without error")
require.Equal(t, "6", objTree.Root().Id)
rawChanges = []*treechangeproto.RawTreeChangeWithId{
changeCreator.CreateRaw("4.a.1", aclList.Head().Id, "4", true, "4"),
changeCreator.CreateRaw("4.a.2", aclList.Head().Id, "4.a.1", true, "4.a.1"),
changeCreator.CreateRaw("4.a.3", aclList.Head().Id, "4.a.2", true, "4.a.2"),
changeCreator.CreateRaw("4.b.1", aclList.Head().Id, "4", true, "4"),
changeCreator.CreateRaw("4.b.2", aclList.Head().Id, "4.b.1", true, "4.b.1"),
changeCreator.CreateRaw("4.b.3", aclList.Head().Id, "4.b.2", true, "4.b.2"),
changeCreator.CreateRaw("5.a.1", aclList.Head().Id, "5", true, "5"),
changeCreator.CreateRaw("5.a.2", aclList.Head().Id, "5.a.1", true, "5.a.1"),
changeCreator.CreateRaw("5.a.3", aclList.Head().Id, "5.a.2", true, "5.a.2"),
changeCreator.CreateRaw("5.b.1", aclList.Head().Id, "5", true, "5"),
changeCreator.CreateRaw("5.b.2", aclList.Head().Id, "5.b.1", true, "5.b.1"),
changeCreator.CreateRaw("5.b.3", aclList.Head().Id, "5.b.2", true, "5.b.2"),
}
payload = RawChangesPayload{
NewHeads: []string{"4.b.3", "4.a.3", "5.b.3", "5.a.3"},
RawChanges: rawChanges,
SnapshotPath: []string{"4", "3", "2", "1", "0"},
}
_, err = objTree.AddRawChanges(context.Background(), payload)
require.NoError(t, err, "adding changes should be without error")
require.Equal(t, "4", objTree.Root().Id)
})
t.Run("their heads before common snapshot", func(t *testing.T) {
// checking that adding old changes did not affect the tree
ctx := prepareTreeContext(t, aclList)

View file

@ -76,6 +76,10 @@ func (tb *treeBuilder) build(opts treeBuilderOpts) (tr *Tree, err error) {
}
} else if !opts.full {
if len(opts.theirSnapshotPath) == 0 {
// this is actually not obvious why we should call this here
// but the idea is if we have no snapshot path, then this can be only in cases
// when we want to use a common snapshot, otherwise we would provide this path
// because we always have a snapshot path
if len(opts.ourSnapshotPath) == 0 {
common, err := tb.storage.CommonSnapshot(tb.ctx)
if err != nil {
@ -83,7 +87,7 @@ func (tb *treeBuilder) build(opts treeBuilderOpts) (tr *Tree, err error) {
}
snapshot = common
} else {
our := opts.ourSnapshotPath[len(opts.ourSnapshotPath)-1]
our := opts.ourSnapshotPath[0]
lowest, err := tb.lowestSnapshots(cache, opts.theirHeads, our)
if err != nil {
return nil, err
@ -161,9 +165,9 @@ func (tb *treeBuilder) lowestSnapshots(cache map[string]*Change, heads []string,
return nil, err
}
if ch.SnapshotId != "" {
next = append(next, ch.SnapshotId)
snapshots = append(snapshots, ch.SnapshotId)
} else if len(ch.PrevIds) == 0 && ch.Id == tb.storage.Id() { // this is a root change
next = append(next, ch.Id)
snapshots = append(snapshots, ch.Id)
} else {
return nil, fmt.Errorf("head with empty snapshot id: %s", ch.Id)
}
@ -172,19 +176,29 @@ func (tb *treeBuilder) lowestSnapshots(cache map[string]*Change, heads []string,
slices.Sort(next)
next = slice.DiscardDuplicatesSorted(next)
current = make([]string, 0, len(next))
var visited []*Change
for len(next) > 0 {
current = current[:0]
current = append(current, next...)
next = next[:0]
for _, id := range current {
if ch, ok := cache[id]; ok {
next = append(current, ch.SnapshotId)
// prevent accidental cycles, which can happen if there is a malicious change
if ch.visited {
return nil, fmt.Errorf("cycle detected in snapshot path: %s", id)
}
ch.visited = true
visited = append(visited, ch)
next = append(next, ch.SnapshotId)
} else {
// this is the lowest snapshot from the ones provided
snapshots = append(snapshots, id)
}
}
}
for _, ch := range visited {
ch.visited = false
}
if ourSnapshot != "" {
snapshots = append(snapshots, ourSnapshot)
}