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

Merge branch 'GO-4709-change-differ' into GO-4146-new-spacestore

This commit is contained in:
mcrakhman 2025-01-10 14:22:03 +01:00
commit e1468c6539
No known key found for this signature in database
GPG key ID: DED12CFEF5B8396B
3 changed files with 306 additions and 3 deletions

View file

@ -0,0 +1,208 @@
package objecttree
import (
"go.uber.org/zap"
"github.com/anyproto/any-sync/util/slice"
)
type (
hasChangesFunc func(ids ...string) bool
treeBuilderFunc func(heads []string) (ReadableObjectTree, error)
onRemoveFunc func(ids []string)
)
type ChangeDiffer struct {
hasChanges hasChangesFunc
attached map[string]*Change
waitList map[string][]*Change
visitedBuf []*Change
}
func NewChangeDiffer(tree ReadableObjectTree, hasChanges hasChangesFunc) (*ChangeDiffer, error) {
diff := &ChangeDiffer{
hasChanges: hasChanges,
attached: make(map[string]*Change),
waitList: make(map[string][]*Change),
}
if tree == nil {
return diff, nil
}
err := tree.IterateRoot(nil, func(c *Change) (isContinue bool) {
diff.add(&Change{
Id: c.Id,
PreviousIds: c.PreviousIds,
})
return true
})
if err != nil {
return nil, err
}
return diff, nil
}
func (d *ChangeDiffer) RemoveBefore(ids []string) (removed []string, notFound []string) {
var attached []*Change
for _, id := range ids {
if ch, ok := d.attached[id]; ok {
attached = append(attached, ch)
continue
}
// check if we have it at the bottom
if !d.hasChanges(id) {
notFound = append(notFound, id)
}
}
d.dfsPrev(attached, func(ch *Change) (isContinue bool) {
removed = append(removed, ch.Id)
return true
}, func(changes []*Change) {
for _, ch := range removed {
delete(d.attached, ch)
}
for _, ch := range d.attached {
ch.Previous = slice.DiscardFromSlice(ch.Previous, func(change *Change) bool {
return change.visited
})
}
})
return
}
func (d *ChangeDiffer) dfsPrev(stack []*Change, visit func(ch *Change) (isContinue bool), afterVisit func([]*Change)) {
d.visitedBuf = d.visitedBuf[:0]
defer func() {
if afterVisit != nil {
afterVisit(d.visitedBuf)
}
for _, ch := range d.visitedBuf {
ch.visited = false
}
}()
for len(stack) > 0 {
ch := stack[len(stack)-1]
stack = stack[:len(stack)-1]
if ch.visited {
continue
}
ch.visited = true
d.visitedBuf = append(d.visitedBuf, ch)
for _, prevCh := range ch.Previous {
if !prevCh.visited {
stack = append(stack, prevCh)
}
}
if !visit(ch) {
return
}
}
}
func (d *ChangeDiffer) Add(changes ...*Change) {
for _, ch := range changes {
d.add(ch)
}
return
}
func (d *ChangeDiffer) add(change *Change) {
_, exists := d.attached[change.Id]
if exists {
return
}
d.attached[change.Id] = change
wl, exists := d.waitList[change.Id]
if exists {
for _, ch := range wl {
ch.Previous = append(ch.Previous, change)
}
delete(d.waitList, change.Id)
}
for _, id := range change.PreviousIds {
prev, exists := d.attached[id]
if exists {
change.Previous = append(change.Previous, prev)
continue
}
wl := d.waitList[id]
wl = append(wl, change)
d.waitList[id] = wl
}
}
type DiffManager struct {
differ *ChangeDiffer
readableTree ReadableObjectTree
notFound map[string]struct{}
onRemove func(ids []string)
heads []string
seenHeads []string
}
func NewDiffManager(initHeads, curHeads []string, treeBuilder treeBuilderFunc, onRemove onRemoveFunc) (*DiffManager, error) {
allHeads := make([]string, 0, len(initHeads)+len(curHeads))
allHeads = append(allHeads, initHeads...)
allHeads = append(allHeads, curHeads...)
readableTree, err := treeBuilder(allHeads)
if err != nil {
return nil, err
}
differ, err := NewChangeDiffer(readableTree, readableTree.HasChanges)
if err != nil {
return nil, err
}
return &DiffManager{
differ: differ,
heads: curHeads,
seenHeads: initHeads,
onRemove: onRemove,
readableTree: readableTree,
notFound: make(map[string]struct{}),
}, nil
}
func (d *DiffManager) Init() {
removed, _ := d.differ.RemoveBefore(d.heads)
d.onRemove(removed)
}
func (d *DiffManager) Remove(ids []string) {
removed, notFound := d.differ.RemoveBefore(ids)
for _, id := range notFound {
d.notFound[id] = struct{}{}
}
d.onRemove(removed)
}
func (d *DiffManager) Add(change *Change) {
d.differ.Add(change)
}
func (d *DiffManager) Update(objTree ObjectTree) {
var (
toAdd = make([]*Change, 0, objTree.Len())
toRemove []string
)
err := objTree.IterateRoot(nil, func(ch *Change) bool {
if ch.IsNew {
toAdd = append(toAdd, &Change{
Id: ch.Id,
PreviousIds: ch.PreviousIds,
})
}
if _, ok := d.notFound[ch.Id]; ok {
toRemove = append(toRemove, ch.Id)
delete(d.notFound, ch.Id)
}
return true
})
if err != nil {
log.Warn("error while iterating over object tree", zap.Error(err))
return
}
d.differ.Add(toAdd...)
d.heads = make([]string, 0, len(d.heads))
d.heads = append(d.heads, objTree.Heads()...)
removed, _ := d.differ.RemoveBefore(toRemove)
d.onRemove(removed)
}

View file

@ -0,0 +1,98 @@
package objecttree
import (
"slices"
"testing"
"github.com/stretchr/testify/require"
)
func TestChangeDiffer_Add(t *testing.T) {
t.Run("remove all", func(t *testing.T) {
changes := []*Change{
newChange("0", ""),
newChange("1", "0", "0"),
newChange("2", "0", "0"),
newChange("3", "0", "1", "2"),
newChange("4", "0", "0"),
newChange("5", "0", "4"),
newChange("6", "0", "5"),
newChange("7", "0", "3", "6"),
}
differ, _ := NewChangeDiffer(nil, func(ids ...string) bool {
return false
})
differ.Add(changes...)
res, notFound := differ.RemoveBefore([]string{"7"})
require.Len(t, notFound, 0)
require.Equal(t, len(changes), len(res))
})
t.Run("remove in two parts", func(t *testing.T) {
changes := []*Change{
newChange("0", ""),
newChange("1", "0", "0"),
newChange("2", "0", "0"),
newChange("3", "0", "1", "2"),
newChange("4", "0", "0"),
newChange("5", "0", "4"),
newChange("6", "0", "5"),
newChange("7", "0", "3", "6"),
}
differ, _ := NewChangeDiffer(nil, func(ids ...string) bool {
return false
})
differ.Add(changes...)
res, notFound := differ.RemoveBefore([]string{"4"})
require.Len(t, notFound, 0)
require.Equal(t, 2, len(res))
res, notFound = differ.RemoveBefore([]string{"7"})
require.Len(t, notFound, 0)
require.Equal(t, 6, len(res))
})
t.Run("add and remove", func(t *testing.T) {
changes := []*Change{
newChange("0", ""),
newChange("1", "0", "0"),
newChange("2", "0", "0"),
newChange("3", "0", "1", "2"),
}
differ, _ := NewChangeDiffer(nil, func(ids ...string) bool {
return false
})
differ.Add(changes...)
res, notFound := differ.RemoveBefore([]string{"3"})
require.Len(t, notFound, 0)
require.Equal(t, len(changes), len(res))
changes = []*Change{
newChange("4", "0", "0"),
newChange("5", "0", "4"),
newChange("6", "0", "5"),
newChange("7", "0", "3", "6"),
}
differ.Add(changes...)
res, notFound = differ.RemoveBefore([]string{"7"})
require.Len(t, notFound, 0)
require.Equal(t, len(changes), len(res))
})
t.Run("remove not found", func(t *testing.T) {
differ, _ := NewChangeDiffer(nil, func(ids ...string) bool {
return false
})
_, notFound := differ.RemoveBefore([]string{"3", "4", "5"})
require.Len(t, notFound, 3)
})
t.Run("exists in storage", func(t *testing.T) {
storedIds := []string{"3", "4", "5"}
differ, _ := NewChangeDiffer(nil, func(ids ...string) bool {
for _, id := range ids {
if !slices.Contains(storedIds, id) {
return false
}
}
return true
})
res, notFound := differ.RemoveBefore([]string{"3", "4", "5"})
require.Len(t, res, 0)
require.Len(t, notFound, 0)
})
}

View file

@ -205,9 +205,6 @@ func (t *Tree) add(c *Change) bool {
if c == nil {
return false
}
if _, exists := t.invalidChanges[c.Id]; exists {
return false
}
if t.root == nil { // first element
t.root = c