WIP history tree
This commit is contained in:
parent
e0db25e2b3
commit
4a0ec1f793
43
commonspace/object/tree/objecttree/historytree.go
Normal file
43
commonspace/object/tree/objecttree/historytree.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package objecttree
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var ErrLoadBeforeRoot = errors.New("can't load before root")
|
||||||
|
|
||||||
|
type HistoryTree interface {
|
||||||
|
RWLocker
|
||||||
|
|
||||||
|
Id() string
|
||||||
|
Root() *Change
|
||||||
|
Heads() []string
|
||||||
|
IterateFrom(id string, convert ChangeConvertFunc, iterate ChangeIterateFunc) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type historyTree struct {
|
||||||
|
*objectTree
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *historyTree) rebuildFromStorage(beforeId string, include bool) (err error) {
|
||||||
|
ot := h.objectTree
|
||||||
|
ot.treeBuilder.Reset()
|
||||||
|
if beforeId == ot.Id() && !include {
|
||||||
|
return ErrLoadBeforeRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
heads := []string{beforeId}
|
||||||
|
if beforeId == "" {
|
||||||
|
heads, err = ot.treeStorage.Heads()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if !include {
|
||||||
|
beforeChange, err := ot.treeBuilder.loadChange(beforeId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
heads = beforeChange.PreviousIds
|
||||||
|
}
|
||||||
|
|
||||||
|
ot.tree, err = ot.treeBuilder.build(heads, nil, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
"github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto"
|
||||||
list2 "github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
list "github.com/anytypeio/any-sync/commonspace/object/acl/list"
|
||||||
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
"github.com/anytypeio/any-sync/commonspace/object/keychain"
|
||||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
"github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto"
|
||||||
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
"github.com/anytypeio/any-sync/commonspace/object/tree/treestorage"
|
||||||
@ -75,7 +75,7 @@ type objectTree struct {
|
|||||||
validator ObjectTreeValidator
|
validator ObjectTreeValidator
|
||||||
rawChangeLoader *rawChangeLoader
|
rawChangeLoader *rawChangeLoader
|
||||||
treeBuilder *treeBuilder
|
treeBuilder *treeBuilder
|
||||||
aclList list2.AclList
|
aclList list.AclList
|
||||||
|
|
||||||
id string
|
id string
|
||||||
rawRoot *treechangeproto.RawTreeChangeWithId
|
rawRoot *treechangeproto.RawTreeChangeWithId
|
||||||
@ -101,13 +101,13 @@ type objectTreeDeps struct {
|
|||||||
treeStorage treestorage.TreeStorage
|
treeStorage treestorage.TreeStorage
|
||||||
validator ObjectTreeValidator
|
validator ObjectTreeValidator
|
||||||
rawChangeLoader *rawChangeLoader
|
rawChangeLoader *rawChangeLoader
|
||||||
aclList list2.AclList
|
aclList list.AclList
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultObjectTreeDeps(
|
func defaultObjectTreeDeps(
|
||||||
rootChange *treechangeproto.RawTreeChangeWithId,
|
rootChange *treechangeproto.RawTreeChangeWithId,
|
||||||
treeStorage treestorage.TreeStorage,
|
treeStorage treestorage.TreeStorage,
|
||||||
aclList list2.AclList) objectTreeDeps {
|
aclList list.AclList) objectTreeDeps {
|
||||||
|
|
||||||
keychain := keychain.NewKeychain()
|
keychain := keychain.NewKeychain()
|
||||||
changeBuilder := NewChangeBuilder(keychain, rootChange)
|
changeBuilder := NewChangeBuilder(keychain, rootChange)
|
||||||
@ -208,7 +208,7 @@ func (ot *objectTree) prepareBuilderContent(content SignableChangeContent) (cnt
|
|||||||
canWrite := state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Writer) ||
|
canWrite := state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Writer) ||
|
||||||
state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Admin)
|
state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Admin)
|
||||||
if !canWrite {
|
if !canWrite {
|
||||||
err = list2.ErrInsufficientPermissions
|
err = list.ErrInsufficientPermissions
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,7 +471,7 @@ func (ot *objectTree) IterateFrom(id string, convert ChangeConvertFunc, iterate
|
|||||||
}
|
}
|
||||||
readKey, exists := ot.keys[c.ReadKeyHash]
|
readKey, exists := ot.keys[c.ReadKeyHash]
|
||||||
if !exists {
|
if !exists {
|
||||||
err = list2.ErrNoReadKey
|
err = list.ErrNoReadKey
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -111,6 +111,24 @@ func prepareAclList(t *testing.T) list.AclList {
|
|||||||
return aclList
|
return aclList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareTreeDeps(aclList list.AclList) (*mockChangeCreator, objectTreeDeps) {
|
||||||
|
changeCreator := &mockChangeCreator{}
|
||||||
|
treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id)
|
||||||
|
root, _ := treeStorage.Root()
|
||||||
|
changeBuilder := &mockChangeBuilder{
|
||||||
|
originalBuilder: NewChangeBuilder(nil, root),
|
||||||
|
}
|
||||||
|
deps := objectTreeDeps{
|
||||||
|
changeBuilder: changeBuilder,
|
||||||
|
treeBuilder: newTreeBuilder(treeStorage, changeBuilder),
|
||||||
|
treeStorage: treeStorage,
|
||||||
|
rawChangeLoader: newRawChangeLoader(treeStorage, changeBuilder),
|
||||||
|
validator: &mockChangeValidator{},
|
||||||
|
aclList: aclList,
|
||||||
|
}
|
||||||
|
return changeCreator, deps
|
||||||
|
}
|
||||||
|
|
||||||
func prepareTreeContext(t *testing.T, aclList list.AclList) testTreeContext {
|
func prepareTreeContext(t *testing.T, aclList list.AclList) testTreeContext {
|
||||||
changeCreator := &mockChangeCreator{}
|
changeCreator := &mockChangeCreator{}
|
||||||
treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id)
|
treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id)
|
||||||
@ -542,4 +560,87 @@ func TestObjectTree(t *testing.T) {
|
|||||||
assert.Equal(t, ch, raw, "the changes in the storage should be the same")
|
assert.Equal(t, ch, raw, "the changes in the storage should be the same")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("test history tree not include", func(t *testing.T) {
|
||||||
|
changeCreator, deps := prepareTreeDeps(aclList)
|
||||||
|
|
||||||
|
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||||
|
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||||
|
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||||
|
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||||
|
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||||
|
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||||
|
changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||||
|
}
|
||||||
|
deps.treeStorage.TransactionAdd(rawChanges, []string{"6"})
|
||||||
|
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||||
|
BeforeId: "6",
|
||||||
|
IncludeBeforeId: false,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// check tree heads
|
||||||
|
assert.Equal(t, []string{"3", "4", "5"}, hTree.Heads())
|
||||||
|
|
||||||
|
// check tree iterate
|
||||||
|
var iterChangesId []string
|
||||||
|
err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool {
|
||||||
|
iterChangesId = append(iterChangesId, change.Id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "iterate should be without error")
|
||||||
|
assert.Equal(t, []string{"0", "1", "2", "3", "4", "5"}, iterChangesId)
|
||||||
|
assert.Equal(t, "0", hTree.Root().Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test history tree include", func(t *testing.T) {
|
||||||
|
changeCreator, deps := prepareTreeDeps(aclList)
|
||||||
|
|
||||||
|
rawChanges := []*treechangeproto.RawTreeChangeWithId{
|
||||||
|
changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"),
|
||||||
|
changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"),
|
||||||
|
changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"),
|
||||||
|
changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"),
|
||||||
|
changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"),
|
||||||
|
changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"),
|
||||||
|
}
|
||||||
|
deps.treeStorage.TransactionAdd(rawChanges, []string{"6"})
|
||||||
|
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||||
|
BeforeId: "6",
|
||||||
|
IncludeBeforeId: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// check tree heads
|
||||||
|
assert.Equal(t, []string{"6"}, hTree.Heads())
|
||||||
|
|
||||||
|
// check tree iterate
|
||||||
|
var iterChangesId []string
|
||||||
|
err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool {
|
||||||
|
iterChangesId = append(iterChangesId, change.Id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "iterate should be without error")
|
||||||
|
assert.Equal(t, []string{"0", "1", "2", "3", "4", "5", "6"}, iterChangesId)
|
||||||
|
assert.Equal(t, "0", hTree.Root().Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("test history tree root", func(t *testing.T) {
|
||||||
|
_, deps := prepareTreeDeps(aclList)
|
||||||
|
hTree, err := buildHistoryTree(deps, HistoryTreeParams{
|
||||||
|
BeforeId: "0",
|
||||||
|
IncludeBeforeId: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// check tree heads
|
||||||
|
assert.Equal(t, []string{"0"}, hTree.Heads())
|
||||||
|
|
||||||
|
// check tree iterate
|
||||||
|
var iterChangesId []string
|
||||||
|
err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool {
|
||||||
|
iterChangesId = append(iterChangesId, change.Id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "iterate should be without error")
|
||||||
|
assert.Equal(t, []string{"0"}, iterChangesId)
|
||||||
|
assert.Equal(t, "0", hTree.Root().Id)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,16 +41,20 @@ func (tb *treeBuilder) Reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (tb *treeBuilder) Build(theirHeads []string, newChanges []*Change) (*Tree, error) {
|
func (tb *treeBuilder) Build(theirHeads []string, newChanges []*Change) (*Tree, error) {
|
||||||
var proposedHeads []string
|
|
||||||
tb.cache = make(map[string]*Change)
|
|
||||||
heads, err := tb.treeStorage.Heads()
|
heads, err := tb.treeStorage.Heads()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return tb.build(heads, theirHeads, newChanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tb *treeBuilder) build(heads []string, theirHeads []string, newChanges []*Change) (*Tree, error) {
|
||||||
|
var proposedHeads []string
|
||||||
|
tb.cache = make(map[string]*Change)
|
||||||
|
|
||||||
// TODO: we can actually get this from tree (though not sure, that there would always be
|
// TODO: we can actually get this from tree (though not sure, that there would always be
|
||||||
// an invariant where the tree has the closest common snapshot of heads)
|
// an invariant where the tree has the closest common snapshot of heads)
|
||||||
// so if optimization is critical we can change this to inject from tree directly
|
// so if optimization is critical we can change this to inject from tree directly,
|
||||||
// but then we have to be sure that invariant stays true
|
// but then we have to be sure that invariant stays true
|
||||||
oldBreakpoint, err := tb.findBreakpoint(heads, true)
|
oldBreakpoint, err := tb.findBreakpoint(heads, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -41,6 +41,15 @@ func BuildObjectTree(treeStorage treestorage.TreeStorage, aclList list.AclList)
|
|||||||
return buildObjectTree(deps)
|
return buildObjectTree(deps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildHistoryTree(params HistoryTreeParams) (HistoryTree, error) {
|
||||||
|
rootChange, err := params.TreeStorage.Root()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
deps := defaultObjectTreeDeps(rootChange, params.TreeStorage, params.AclList)
|
||||||
|
return buildHistoryTree(deps, params)
|
||||||
|
}
|
||||||
|
|
||||||
func CreateDerivedObjectTree(
|
func CreateDerivedObjectTree(
|
||||||
payload ObjectTreeCreatePayload,
|
payload ObjectTreeCreatePayload,
|
||||||
aclList list.AclList,
|
aclList list.AclList,
|
||||||
@ -118,7 +127,6 @@ func buildObjectTree(deps objectTreeDeps) (ObjectTree, error) {
|
|||||||
aclList: deps.aclList,
|
aclList: deps.aclList,
|
||||||
changeBuilder: deps.changeBuilder,
|
changeBuilder: deps.changeBuilder,
|
||||||
rawChangeLoader: deps.rawChangeLoader,
|
rawChangeLoader: deps.rawChangeLoader,
|
||||||
tree: nil,
|
|
||||||
keys: make(map[uint64]*symmetric.Key),
|
keys: make(map[uint64]*symmetric.Key),
|
||||||
newChangesBuf: make([]*Change, 0, 10),
|
newChangesBuf: make([]*Change, 0, 10),
|
||||||
difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10),
|
difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10),
|
||||||
@ -146,3 +154,38 @@ func buildObjectTree(deps objectTreeDeps) (ObjectTree, error) {
|
|||||||
|
|
||||||
return objTree, nil
|
return objTree, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HistoryTreeParams struct {
|
||||||
|
TreeStorage treestorage.TreeStorage
|
||||||
|
AclList list.AclList
|
||||||
|
BeforeId string
|
||||||
|
IncludeBeforeId bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildHistoryTree(deps objectTreeDeps, params HistoryTreeParams) (ht HistoryTree, err error) {
|
||||||
|
objTree := &objectTree{
|
||||||
|
treeStorage: deps.treeStorage,
|
||||||
|
treeBuilder: deps.treeBuilder,
|
||||||
|
validator: deps.validator,
|
||||||
|
aclList: deps.aclList,
|
||||||
|
changeBuilder: deps.changeBuilder,
|
||||||
|
rawChangeLoader: deps.rawChangeLoader,
|
||||||
|
keys: make(map[uint64]*symmetric.Key),
|
||||||
|
newChangesBuf: make([]*Change, 0, 10),
|
||||||
|
difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10),
|
||||||
|
notSeenIdxBuf: make([]int, 0, 10),
|
||||||
|
newSnapshotsBuf: make([]*Change, 0, 10),
|
||||||
|
}
|
||||||
|
|
||||||
|
hTree := historyTree{objectTree: objTree}
|
||||||
|
err = hTree.rebuildFromStorage(params.BeforeId, params.IncludeBeforeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
objTree.id = objTree.treeStorage.Id()
|
||||||
|
objTree.rawRoot, err = objTree.treeStorage.Root()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return hTree, nil
|
||||||
|
}
|
||||||
@ -87,6 +87,7 @@ type Space interface {
|
|||||||
PutTree(ctx context.Context, payload treestorage.TreeStorageCreatePayload, listener updatelistener.UpdateListener) (t objecttree.ObjectTree, err error)
|
PutTree(ctx context.Context, payload treestorage.TreeStorageCreatePayload, listener updatelistener.UpdateListener) (t objecttree.ObjectTree, err error)
|
||||||
BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error)
|
BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error)
|
||||||
DeleteTree(ctx context.Context, id string) (err error)
|
DeleteTree(ctx context.Context, id string) (err error)
|
||||||
|
BuildHistoryTree(ctx context.Context, id string, opts HistoryTreeOpts) (t objecttree.HistoryTree, err error)
|
||||||
|
|
||||||
HeadSync() headsync.HeadSync
|
HeadSync() headsync.HeadSync
|
||||||
SyncStatus() syncstatus.StatusUpdater
|
SyncStatus() syncstatus.StatusUpdater
|
||||||
@ -296,6 +297,11 @@ type BuildTreeOpts struct {
|
|||||||
WaitTreeRemoteSync bool
|
WaitTreeRemoteSync bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HistoryTreeOpts struct {
|
||||||
|
BeforeId string
|
||||||
|
Include bool
|
||||||
|
}
|
||||||
|
|
||||||
func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error) {
|
func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error) {
|
||||||
if s.isClosed.Load() {
|
if s.isClosed.Load() {
|
||||||
err = ErrSpaceClosed
|
err = ErrSpaceClosed
|
||||||
@ -317,6 +323,24 @@ func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t
|
|||||||
return synctree.BuildSyncTreeOrGetRemote(ctx, id, deps)
|
return synctree.BuildSyncTreeOrGetRemote(ctx, id, deps)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *space) BuildHistoryTree(ctx context.Context, id string, opts HistoryTreeOpts) (t objecttree.HistoryTree, err error) {
|
||||||
|
if s.isClosed.Load() {
|
||||||
|
err = ErrSpaceClosed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params := objecttree.HistoryTreeParams{
|
||||||
|
AclList: s.aclList,
|
||||||
|
BeforeId: opts.BeforeId,
|
||||||
|
IncludeBeforeId: opts.Include,
|
||||||
|
}
|
||||||
|
params.TreeStorage, err = s.storage.TreeStorage(id)
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return objecttree.BuildHistoryTree(params)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *space) DeleteTree(ctx context.Context, id string) (err error) {
|
func (s *space) DeleteTree(ctx context.Context, id string) (err error) {
|
||||||
return s.settingsObject.DeleteObject(id)
|
return s.settingsObject.DeleteObject(id)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user