Pilorama fixes for support/v0.38 #1255

Merged
fyrchik merged 6 commits from fyrchik/frostfs-node:fix-pilorama into support/v0.38 2024-09-04 19:51:10 +00:00
23 changed files with 598 additions and 277 deletions

View file

@ -66,7 +66,7 @@ func move(cmd *cobra.Command, _ []string) {
Body: &tree.GetSubTreeRequest_Body{ Body: &tree.GetSubTreeRequest_Body{
ContainerId: rawCID, ContainerId: rawCID,
TreeId: tid, TreeId: tid,
RootId: nid, RootId: []uint64{nid},
Depth: 1, Depth: 1,
BearerToken: bt, BearerToken: bt,
}, },

View file

@ -68,7 +68,7 @@ func getSubTree(cmd *cobra.Command, _ []string) {
Body: &tree.GetSubTreeRequest_Body{ Body: &tree.GetSubTreeRequest_Body{
ContainerId: rawCID, ContainerId: rawCID,
TreeId: tid, TreeId: tid,
RootId: rid, RootId: []uint64{rid},
Depth: depth, Depth: depth,
BearerToken: bt, BearerToken: bt,
}, },
@ -83,10 +83,15 @@ func getSubTree(cmd *cobra.Command, _ []string) {
for ; err == nil; subtreeResp, err = resp.Recv() { for ; err == nil; subtreeResp, err = resp.Recv() {
b := subtreeResp.GetBody() b := subtreeResp.GetBody()
if len(b.GetNodeId()) == 1 {
cmd.Printf("Node ID: %d\n", b.GetNodeId()) cmd.Printf("Node ID: %d\n", b.GetNodeId())
cmd.Println("\tParent ID: ", b.GetParentId()) cmd.Println("\tParent ID: ", b.GetParentId())
cmd.Println("\tTimestamp: ", b.GetTimestamp()) cmd.Println("\tTimestamp: ", b.GetTimestamp())
} else {
cmd.Printf("Node IDs: %v\n", b.GetNodeId())
cmd.Println("\tParent IDs: ", b.GetParentId())
cmd.Println("\tTimestamps: ", b.GetTimestamp())
}
if meta := b.GetMeta(); len(meta) > 0 { if meta := b.GetMeta(); len(meta) > 0 {
cmd.Println("\tMeta pairs: ") cmd.Println("\tMeta pairs: ")

26
go.mod
View file

@ -4,7 +4,7 @@ go 1.20
require ( require (
code.gitea.io/sdk/gitea v0.17.1 code.gitea.io/sdk/gitea v0.17.1
git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240215124401-634e24aba715 git.frostfs.info/TrueCloudLab/frostfs-api-go/v2 v2.16.1-0.20240717110908-4e13f713f156
git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45 git.frostfs.info/TrueCloudLab/frostfs-contract v0.19.3-0.20240409111539-e7a05a49ff45
git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65 git.frostfs.info/TrueCloudLab/frostfs-observability v0.0.0-20231101111734-b3ad3335ff65
git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240301150205-6fe4e2541d0b git.frostfs.info/TrueCloudLab/frostfs-sdk-go v0.0.0-20240301150205-6fe4e2541d0b
@ -38,11 +38,11 @@ require (
go.opentelemetry.io/otel/trace v1.22.0 go.opentelemetry.io/otel/trace v1.22.0
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/sync v0.6.0 golang.org/x/sync v0.7.0
golang.org/x/sys v0.18.0 golang.org/x/sys v0.22.0
golang.org/x/term v0.18.0 golang.org/x/term v0.22.0
google.golang.org/grpc v1.61.0 google.golang.org/grpc v1.61.2
google.golang.org/protobuf v1.33.0 google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@ -60,7 +60,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.2-0.20231222162921-eb75782795d2 // indirect github.com/consensys/gnark-crypto v0.12.2-0.20231222162921-eb75782795d2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
@ -71,7 +71,7 @@ require (
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/websocket v1.5.1 // indirect github.com/gorilla/websocket v1.5.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0 // indirect
@ -121,11 +121,11 @@ require (
go.opentelemetry.io/otel/sdk v1.22.0 // indirect go.opentelemetry.io/otel/sdk v1.22.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.23.0 // indirect golang.org/x/net v0.27.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
lukechampine.com/blake3 v1.2.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect
rsc.io/tmplfunc v0.0.3 // indirect rsc.io/tmplfunc v0.0.3 // indirect

BIN
go.sum

Binary file not shown.

View file

@ -210,18 +210,17 @@ func (e *StorageEngine) TreeGetChildren(ctx context.Context, cid cidSDK.ID, tree
} }
// TreeSortedByFilename implements the pilorama.Forest interface. // TreeSortedByFilename implements the pilorama.Forest interface.
func (e *StorageEngine) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID pilorama.Node, last *string, count int) ([]pilorama.NodeInfo, *string, error) { func (e *StorageEngine) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID pilorama.MultiNode, last *string, count int) ([]pilorama.MultiNodeInfo, *string, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "StorageEngine.TreeSortedByFilename", ctx, span := tracing.StartSpanFromContext(ctx, "StorageEngine.TreeSortedByFilename",
trace.WithAttributes( trace.WithAttributes(
attribute.String("container_id", cid.EncodeToString()), attribute.String("container_id", cid.EncodeToString()),
attribute.String("tree_id", treeID), attribute.String("tree_id", treeID),
attribute.String("node_id", strconv.FormatUint(nodeID, 10)),
), ),
) )
defer span.End() defer span.End()
var err error var err error
var nodes []pilorama.NodeInfo var nodes []pilorama.MultiNodeInfo
var cursor *string var cursor *string
for _, sh := range e.sortShards(cid) { for _, sh := range e.sortShards(cid) {
nodes, cursor, err = sh.TreeSortedByFilename(ctx, cid, treeID, nodeID, last, count) nodes, cursor, err = sh.TreeSortedByFilename(ctx, cid, treeID, nodeID, last, count)

View file

@ -906,7 +906,7 @@ func (t *boltForest) TreeGetByPath(ctx context.Context, cid cidSDK.ID, treeID st
b := treeRoot.Bucket(dataBucket) b := treeRoot.Bucket(dataBucket)
i, curNode, err := t.getPathPrefix(b, attr, path[:len(path)-1]) i, curNodes, err := t.getPathPrefixMultiTraversal(b, attr, path[:len(path)-1])
if err != nil { if err != nil {
return err return err
} }
@ -918,7 +918,8 @@ func (t *boltForest) TreeGetByPath(ctx context.Context, cid cidSDK.ID, treeID st
c := b.Cursor() c := b.Cursor()
attrKey := internalKey(nil, attr, path[len(path)-1], curNode, 0) for i := range curNodes {
attrKey := internalKey(nil, attr, path[len(path)-1], curNodes[i], 0)
attrKey = attrKey[:len(attrKey)-8] attrKey = attrKey[:len(attrKey)-8]
childKey, _ := c.Seek(attrKey) childKey, _ := c.Seek(attrKey)
for len(childKey) == len(attrKey)+8 && bytes.Equal(attrKey, childKey[:len(childKey)-8]) { for len(childKey) == len(attrKey)+8 && bytes.Equal(attrKey, childKey[:len(childKey)-8]) {
@ -934,6 +935,7 @@ func (t *boltForest) TreeGetByPath(ctx context.Context, cid cidSDK.ID, treeID st
} }
childKey, _ = c.Next() childKey, _ = c.Next()
} }
}
return nil return nil
})) }))
success = err == nil success = err == nil
@ -988,23 +990,26 @@ func (t *boltForest) TreeGetMeta(ctx context.Context, cid cidSDK.ID, treeID stri
return m, parentID, metaerr.Wrap(err) return m, parentID, metaerr.Wrap(err)
} }
func (t *boltForest) hasFewChildren(b *bbolt.Bucket, nodeID Node, threshold int) bool { func (t *boltForest) hasFewChildren(b *bbolt.Bucket, nodeIDs MultiNode, threshold int) bool {
key := make([]byte, 9) key := make([]byte, 9)
key[0] = 'c' key[0] = 'c'
binary.LittleEndian.PutUint64(key[1:], nodeID)
count := 0 count := 0
for _, nodeID := range nodeIDs {
binary.LittleEndian.PutUint64(key[1:], nodeID)
c := b.Cursor() c := b.Cursor()
for k, _ := c.Seek(key); len(k) == childrenKeySize && binary.LittleEndian.Uint64(k[1:]) == nodeID; k, _ = c.Next() { for k, _ := c.Seek(key); len(k) == childrenKeySize && binary.LittleEndian.Uint64(k[1:]) == nodeID; k, _ = c.Next() {
if count++; count > threshold { if count++; count > threshold {
return false return false
} }
} }
}
return true return true
} }
// TreeSortedByFilename implements the Forest interface. // TreeSortedByFilename implements the Forest interface.
func (t *boltForest) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID Node, last *string, count int) ([]NodeInfo, *string, error) { func (t *boltForest) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeIDs MultiNode, last *string, count int) ([]MultiNodeInfo, *string, error) {
var ( var (
startedAt = time.Now() startedAt = time.Now()
success = false success = false
@ -1017,7 +1022,6 @@ func (t *boltForest) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, tr
trace.WithAttributes( trace.WithAttributes(
attribute.String("container_id", cid.EncodeToString()), attribute.String("container_id", cid.EncodeToString()),
attribute.String("tree_id", treeID), attribute.String("tree_id", treeID),
attribute.String("node_id", strconv.FormatUint(nodeID, 10)),
), ),
) )
defer span.End() defer span.End()
@ -1028,6 +1032,9 @@ func (t *boltForest) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, tr
if t.mode.NoMetabase() { if t.mode.NoMetabase() {
return nil, last, ErrDegradedMode return nil, last, ErrDegradedMode
} }
if len(nodeIDs) == 0 {
return nil, last, errors.New("empty node list")
}
h := newHeap(last, count) h := newHeap(last, count)
key := make([]byte, 9) key := make([]byte, 9)
@ -1046,21 +1053,23 @@ func (t *boltForest) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, tr
// If the node is a leaf, we could scan all filenames in the tree. // If the node is a leaf, we could scan all filenames in the tree.
// To prevent this we first count the number of children: if it is less than // To prevent this we first count the number of children: if it is less than
// the number of nodes we need to return, fallback to TreeGetChildren() implementation. // the number of nodes we need to return, fallback to TreeGetChildren() implementation.
if fewChildren = t.hasFewChildren(b, nodeID, count); fewChildren { if fewChildren = t.hasFewChildren(b, nodeIDs, count); fewChildren {
var err error var err error
result, err = t.getChildren(b, nodeID) result, err = t.getChildren(b, nodeIDs)
return err return err
} }
t.fillSortedChildren(b, nodeID, h) t.fillSortedChildren(b, nodeIDs, h)
for info, ok := h.pop(); ok; info, ok = h.pop() { for info, ok := h.pop(); ok; info, ok = h.pop() {
childInfo, err := t.getChildInfo(b, key, info.id) for _, id := range info.id {
childInfo, err := t.getChildInfo(b, key, id)
if err != nil { if err != nil {
return err return err
} }
result = append(result, childInfo) result = append(result, childInfo)
} }
}
return nil return nil
}) })
@ -1072,11 +1081,15 @@ func (t *boltForest) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, tr
if fewChildren { if fewChildren {
result = sortAndCut(result, last) result = sortAndCut(result, last)
} }
if len(result) != 0 { res := mergeNodeInfos(result)
s := string(result[len(result)-1].Meta.GetAttr(AttributeFilename)) if len(res) > count {
res = res[:count]
}
if len(res) != 0 {
s := string(findAttr(res[len(res)-1].Meta, AttributeFilename))
last = &s last = &s
} }
return result, last, metaerr.Wrap(err) return res, last, metaerr.Wrap(err)
} }
func sortAndCut(result []NodeInfo, last *string) []NodeInfo { func sortAndCut(result []NodeInfo, last *string) []NodeInfo {
@ -1107,37 +1120,64 @@ func (t *boltForest) getChildInfo(b *bbolt.Bucket, key []byte, childID Node) (No
return childInfo, nil return childInfo, nil
} }
func (t *boltForest) fillSortedChildren(b *bbolt.Bucket, nodeID Node, h *fixedHeap) { func (t *boltForest) fillSortedChildren(b *bbolt.Bucket, nodeIDs MultiNode, h *fixedHeap) {
c := b.Cursor() c := b.Cursor()
prefix := internalKeyPrefix(nil, AttributeFilename) prefix := internalKeyPrefix(nil, AttributeFilename)
length := uint16(0) length := uint16(0)
count := 0 count := 0
var nodes []uint64
var lastFilename *string
for k, _ := c.Seek(prefix); len(k) > 0 && k[0] == 'i'; k, _ = c.Next() { for k, _ := c.Seek(prefix); len(k) > 0 && k[0] == 'i'; k, _ = c.Next() {
if len(k) < len(prefix)+2+16 { if len(k) < len(prefix)+2+16 {
continue continue
} }
parentID := binary.LittleEndian.Uint64(k[len(k)-16:]) parentID := binary.LittleEndian.Uint64(k[len(k)-16:])
if parentID != nodeID {
var contains bool
Review

This has changed compared to master (slices.Contains) because of go1.20 requirement in this branch.
Please, re-check the correctness.

This has changed compared to master (`slices.Contains`) because of go1.20 requirement in this branch. Please, re-check the correctness.
for i := range nodeIDs {
if parentID == nodeIDs[i] {
contains = true
break
}
}
if !contains {
continue continue
} }
actualLength := binary.LittleEndian.Uint16(k[len(prefix):]) actualLength := binary.LittleEndian.Uint16(k[len(prefix):])
childID := binary.LittleEndian.Uint64(k[len(k)-8:]) childID := binary.LittleEndian.Uint64(k[len(k)-8:])
filename := string(k[len(prefix)+2 : len(k)-16]) filename := string(k[len(prefix)+2 : len(k)-16])
processed := h.push(childID, filename)
if lastFilename == nil {
lastFilename = &filename
nodes = append(nodes, childID)
} else if *lastFilename == filename {
nodes = append(nodes, childID)
} else {
processed := h.push(nodes, *lastFilename)
nodes = MultiNode{childID}
lastFilename = &filename
if actualLength != length { if actualLength != length {
length = actualLength length = actualLength
count = 1 count = 1
} else if processed { } else if processed {
if count++; count > h.count { if count++; count > h.count {
lastFilename = nil
nodes = nil
length = actualLength + 1 length = actualLength + 1
c.Seek(append(prefix, byte(length), byte(length>>8))) c.Seek(append(prefix, byte(length), byte(length>>8)))
c.Prev() // c.Next() will be performed by for loop c.Prev() // c.Next() will be performed by for loop
} }
} }
} }
}
if len(nodes) != 0 && lastFilename != nil {
h.push(nodes, *lastFilename)
}
} }
// TreeGetChildren implements the Forest interface. // TreeGetChildren implements the Forest interface.
@ -1177,17 +1217,18 @@ func (t *boltForest) TreeGetChildren(ctx context.Context, cid cidSDK.ID, treeID
b := treeRoot.Bucket(dataBucket) b := treeRoot.Bucket(dataBucket)
var err error var err error
result, err = t.getChildren(b, nodeID) result, err = t.getChildren(b, []Node{nodeID})
return err return err
}) })
success = err == nil success = err == nil
return result, metaerr.Wrap(err) return result, metaerr.Wrap(err)
} }
func (t *boltForest) getChildren(b *bbolt.Bucket, nodeID Node) ([]NodeInfo, error) { func (t *boltForest) getChildren(b *bbolt.Bucket, nodeIDs MultiNode) ([]NodeInfo, error) {
var result []NodeInfo var result []NodeInfo
key := make([]byte, 9) key := make([]byte, 9)
for _, nodeID := range nodeIDs {
key[0] = 'c' key[0] = 'c'
binary.LittleEndian.PutUint64(key[1:], nodeID) binary.LittleEndian.PutUint64(key[1:], nodeID)
@ -1200,6 +1241,7 @@ func (t *boltForest) getChildren(b *bbolt.Bucket, nodeID Node) ([]NodeInfo, erro
} }
result = append(result, childInfo) result = append(result, childInfo)
} }
}
return result, nil return result, nil
} }
@ -1412,6 +1454,36 @@ func (t *boltForest) TreeListTrees(ctx context.Context, prm TreeListTreesPrm) (*
return &res, nil return &res, nil
} }
func (t *boltForest) getPathPrefixMultiTraversal(bTree *bbolt.Bucket, attr string, path []string) (int, []Node, error) {
c := bTree.Cursor()
var curNodes []Node
nextNodes := []Node{RootID}
var attrKey []byte
for i := range path {
curNodes, nextNodes = nextNodes, curNodes[:0]
for j := range curNodes {
attrKey = internalKey(attrKey, attr, path[i], curNodes[j], 0)
attrKey = attrKey[:len(attrKey)-8]
childKey, value := c.Seek(attrKey)
for len(childKey) == len(attrKey)+8 && bytes.Equal(attrKey, childKey[:len(childKey)-8]) {
if len(value) == 1 && value[0] == 1 {
nextNodes = append(nextNodes, binary.LittleEndian.Uint64(childKey[len(childKey)-8:]))
}
childKey, value = c.Next()
}
}
if len(nextNodes) == 0 {
return i, curNodes, nil
}
}
return len(path), nextNodes, nil
}
func (t *boltForest) getPathPrefix(bTree *bbolt.Bucket, attr string, path []string) (int, Node, error) { func (t *boltForest) getPathPrefix(bTree *bbolt.Bucket, attr string, path []string) (int, Node, error) {
c := bTree.Cursor() c := bTree.Cursor()

View file

@ -156,7 +156,7 @@ func (f *memoryForest) TreeGetMeta(_ context.Context, cid cid.ID, treeID string,
} }
// TreeSortedByFilename implements the Forest interface. // TreeSortedByFilename implements the Forest interface.
func (f *memoryForest) TreeSortedByFilename(_ context.Context, cid cid.ID, treeID string, nodeID Node, start *string, count int) ([]NodeInfo, *string, error) { func (f *memoryForest) TreeSortedByFilename(_ context.Context, cid cid.ID, treeID string, nodeIDs MultiNode, start *string, count int) ([]MultiNodeInfo, *string, error) {
fullID := cid.String() + "/" + treeID fullID := cid.String() + "/" + treeID
s, ok := f.treeMap[fullID] s, ok := f.treeMap[fullID]
if !ok { if !ok {
@ -166,8 +166,10 @@ func (f *memoryForest) TreeSortedByFilename(_ context.Context, cid cid.ID, treeI
return nil, start, nil return nil, start, nil
} }
var res []NodeInfo
for _, nodeID := range nodeIDs {
children := s.tree.getChildren(nodeID) children := s.tree.getChildren(nodeID)
res := make([]NodeInfo, 0, len(children))
for _, childID := range children { for _, childID := range children {
var found bool var found bool
for _, kv := range s.infoMap[childID].Meta.Items { for _, kv := range s.infoMap[childID].Meta.Items {
@ -185,21 +187,24 @@ func (f *memoryForest) TreeSortedByFilename(_ context.Context, cid cid.ID, treeI
ParentID: s.infoMap[childID].Parent, ParentID: s.infoMap[childID].Parent,
}) })
} }
}
if len(res) == 0 { if len(res) == 0 {
return res, start, nil return nil, start, nil
} }
sort.Slice(res, func(i, j int) bool { sort.Slice(res, func(i, j int) bool {
return bytes.Compare(res[i].Meta.GetAttr(AttributeFilename), res[j].Meta.GetAttr(AttributeFilename)) == -1 return bytes.Compare(res[i].Meta.GetAttr(AttributeFilename), res[j].Meta.GetAttr(AttributeFilename)) == -1
}) })
for i := range res {
if start == nil || string(res[i].Meta.GetAttr(AttributeFilename)) > *start { r := mergeNodeInfos(res)
for i := range r {
if start == nil || string(findAttr(r[i].Meta, AttributeFilename)) > *start {
finish := i + count finish := i + count
if len(res) < finish { if len(res) < finish {
finish = len(res) finish = len(res)
} }
last := string(res[finish-1].Meta.GetAttr(AttributeFilename)) last := string(findAttr(r[finish-1].Meta, AttributeFilename))
return res[i:finish], &last, nil return r[i:finish], &last, nil
} }
} }
last := string(res[len(res)-1].Meta.GetAttr(AttributeFilename)) last := string(res[len(res)-1].Meta.GetAttr(AttributeFilename))

View file

@ -215,7 +215,7 @@ func BenchmarkForestSortedIteration(b *testing.B) {
b.Run(providers[i].name+",root", func(b *testing.B) { b.Run(providers[i].name+",root", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
res, _, err := f.TreeSortedByFilename(context.Background(), cnr, treeID, RootID, nil, 100) res, _, err := f.TreeSortedByFilename(context.Background(), cnr, treeID, MultiNode{RootID}, nil, 100)
if err != nil || len(res) != 100 { if err != nil || len(res) != 100 {
b.Fatalf("err %v, count %d", err, len(res)) b.Fatalf("err %v, count %d", err, len(res))
} }
@ -223,7 +223,7 @@ func BenchmarkForestSortedIteration(b *testing.B) {
}) })
b.Run(providers[i].name+",leaf", func(b *testing.B) { b.Run(providers[i].name+",leaf", func(b *testing.B) {
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
res, _, err := f.TreeSortedByFilename(context.Background(), cnr, treeID, 1, nil, 100) res, _, err := f.TreeSortedByFilename(context.Background(), cnr, treeID, MultiNode{1}, nil, 100)
if err != nil || len(res) != 0 { if err != nil || len(res) != 0 {
b.FailNow() b.FailNow()
} }
@ -266,9 +266,9 @@ func testForestTreeSortedIteration(t *testing.T, s ForestStorage) {
treeAdd(t, i+1, strconv.Itoa(i+1)) treeAdd(t, i+1, strconv.Itoa(i+1))
} }
var result []NodeInfo var result []MultiNodeInfo
treeAppend := func(t *testing.T, last *string, count int) *string { treeAppend := func(t *testing.T, last *string, count int) *string {
res, cursor, err := s.TreeSortedByFilename(context.Background(), d.CID, treeID, RootID, last, count) res, cursor, err := s.TreeSortedByFilename(context.Background(), d.CID, treeID, MultiNode{RootID}, last, count)
require.NoError(t, err) require.NoError(t, err)
result = append(result, res...) result = append(result, res...)
return cursor return cursor
@ -282,11 +282,11 @@ func testForestTreeSortedIteration(t *testing.T, s ForestStorage) {
require.Len(t, result, count) require.Len(t, result, count)
for i := range result { for i := range result {
require.Equal(t, RootID+uint64(i+1), result[i].ID) require.Equal(t, MultiNode{RootID + uint64(i+1)}, result[i].Children)
if i == 0 { if i == 0 {
require.Equal(t, "", string(result[i].Meta.GetAttr(AttributeFilename))) require.Equal(t, "", string(findAttr(result[i].Meta, AttributeFilename)))
} else { } else {
require.Equal(t, strconv.Itoa(RootID+i+1), string(result[i].Meta.GetAttr(AttributeFilename))) require.Equal(t, strconv.Itoa(RootID+i+1), string(findAttr(result[i].Meta, AttributeFilename)))
} }
} }
} }
@ -318,12 +318,12 @@ func testForestTreeSortedByFilename(t *testing.T, s ForestStorage) {
require.NoError(t, err) require.NoError(t, err)
} }
expectAttributes := func(t *testing.T, attr string, expected []string, res []NodeInfo) { expectAttributes := func(t *testing.T, attr string, expected []string, res []MultiNodeInfo) {
require.Equal(t, len(expected), len(res)) require.Equal(t, len(expected), len(res))
actual := make([]string, len(res)) actual := make([]string, len(res))
for i := range actual { for i := range actual {
actual[i] = string(res[i].Meta.GetAttr(attr)) actual[i] = string(findAttr(res[i].Meta, attr))
} }
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
} }
@ -345,40 +345,40 @@ func testForestTreeSortedByFilename(t *testing.T, s ForestStorage) {
treeAddByPath(t, items[i]) treeAddByPath(t, items[i])
} }
getChildren := func(t *testing.T, id Node) []NodeInfo { getChildren := func(t *testing.T, id MultiNode) []MultiNodeInfo {
res, _, err := s.TreeSortedByFilename(context.Background(), d.CID, treeID, id, nil, len(items)) res, _, err := s.TreeSortedByFilename(context.Background(), d.CID, treeID, id, nil, len(items))
require.NoError(t, err) require.NoError(t, err)
return res return res
} }
res := getChildren(t, RootID) res := getChildren(t, MultiNode{RootID})
expectAttributes(t, AttributeFilename, []string{"a", "b", "c"}, res) expectAttributes(t, AttributeFilename, []string{"a", "b", "c"}, res)
expectAttributes(t, controlAttr, []string{"", "", "c"}, res) expectAttributes(t, controlAttr, []string{"", "", "c"}, res)
{ {
ra := getChildren(t, res[0].ID) ra := getChildren(t, res[0].Children)
expectAttributes(t, AttributeFilename, []string{"bbb"}, ra) expectAttributes(t, AttributeFilename, []string{"bbb"}, ra)
expectAttributes(t, controlAttr, []string{""}, ra) expectAttributes(t, controlAttr, []string{""}, ra)
rabbb := getChildren(t, ra[0].ID) rabbb := getChildren(t, ra[0].Children)
expectAttributes(t, AttributeFilename, []string{"ccc", "xxx", "z"}, rabbb) expectAttributes(t, AttributeFilename, []string{"ccc", "xxx", "z"}, rabbb)
expectAttributes(t, controlAttr, []string{"a/bbb/ccc", "a/bbb/xxx", "a/bbb/z"}, rabbb) expectAttributes(t, controlAttr, []string{"a/bbb/ccc", "a/bbb/xxx", "a/bbb/z"}, rabbb)
} }
{ {
rb := getChildren(t, res[1].ID) rb := getChildren(t, res[1].Children)
expectAttributes(t, AttributeFilename, []string{"bbb", "xxx"}, rb) expectAttributes(t, AttributeFilename, []string{"bbb", "xxx"}, rb)
expectAttributes(t, controlAttr, []string{"", ""}, rb) expectAttributes(t, controlAttr, []string{"", ""}, rb)
rbbbb := getChildren(t, rb[0].ID) rbbbb := getChildren(t, rb[0].Children)
expectAttributes(t, AttributeFilename, []string{"ccc"}, rbbbb) expectAttributes(t, AttributeFilename, []string{"ccc"}, rbbbb)
expectAttributes(t, controlAttr, []string{"b/bbb/ccc"}, rbbbb) expectAttributes(t, controlAttr, []string{"b/bbb/ccc"}, rbbbb)
rbxxx := getChildren(t, rb[1].ID) rbxxx := getChildren(t, rb[1].Children)
expectAttributes(t, AttributeFilename, []string{"z"}, rbxxx) expectAttributes(t, AttributeFilename, []string{"z"}, rbxxx)
expectAttributes(t, controlAttr, []string{"b/xxx/z"}, rbxxx) expectAttributes(t, controlAttr, []string{"b/xxx/z"}, rbxxx)
} }
{ {
rc := getChildren(t, res[2].ID) rc := getChildren(t, res[2].Children)
require.Len(t, rc, 0) require.Len(t, rc, 0)
} }
} }

View file

@ -5,7 +5,7 @@ import (
) )
type heapInfo struct { type heapInfo struct {
id Node id MultiNode
filename string filename string
} }
@ -46,7 +46,7 @@ func newHeap(start *string, count int) *fixedHeap {
} }
} }
func (h *fixedHeap) push(id Node, filename string) bool { func (h *fixedHeap) push(id MultiNode, filename string) bool {
if h.start != nil && filename <= *h.start { if h.start != nil && filename <= *h.start {
return false return false
} }

View file

@ -35,7 +35,7 @@ type Forest interface {
TreeGetChildren(ctx context.Context, cid cidSDK.ID, treeID string, nodeID Node) ([]NodeInfo, error) TreeGetChildren(ctx context.Context, cid cidSDK.ID, treeID string, nodeID Node) ([]NodeInfo, error)
// TreeSortedByFilename returns children of the node with the specified ID. The nodes are sorted by the filename attribute.. // TreeSortedByFilename returns children of the node with the specified ID. The nodes are sorted by the filename attribute..
// Should return ErrTreeNotFound if the tree is not found, and empty result if the node is not in the tree. // Should return ErrTreeNotFound if the tree is not found, and empty result if the node is not in the tree.
TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID Node, last *string, count int) ([]NodeInfo, *string, error) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID MultiNode, last *string, count int) ([]MultiNodeInfo, *string, error)
// TreeGetOpLog returns first log operation stored at or above the height. // TreeGetOpLog returns first log operation stored at or above the height.
// In case no such operation is found, empty Move and nil error should be returned. // In case no such operation is found, empty Move and nil error should be returned.
TreeGetOpLog(ctx context.Context, cid cidSDK.ID, treeID string, height uint64) (Move, error) TreeGetOpLog(ctx context.Context, cid cidSDK.ID, treeID string, height uint64) (Move, error)

View file

@ -21,7 +21,11 @@ func (x Meta) Bytes() []byte {
} }
func (x Meta) GetAttr(name string) []byte { func (x Meta) GetAttr(name string) []byte {
for _, kv := range x.Items { return findAttr(x.Items, name)
}
func findAttr(ms []KeyValue, name string) []byte {
for _, kv := range ms {
if kv.Key == name { if kv.Key == name {
return kv.Value return kv.Value
} }

View file

@ -0,0 +1,49 @@
package pilorama
import "bytes"
// MultiNode represents a group of internal nodes accessible by the same path, but having different id.
type MultiNode []Node
// MultiNodeInfo represents a group of internal nodes accessible by the same path, but having different id.
type MultiNodeInfo struct {
Children MultiNode
Parents MultiNode
Timestamps []uint64
Meta []KeyValue
}
func (r *MultiNodeInfo) Add(info NodeInfo) bool {
if !isInternal(info.Meta.Items) || !isInternal(r.Meta) ||
!bytes.Equal(r.Meta[0].Value, info.Meta.Items[0].Value) {
return false
}
r.Children = append(r.Children, info.ID)
r.Parents = append(r.Parents, info.ParentID)
r.Timestamps = append(r.Timestamps, info.Meta.Time)
return true
}
func (n NodeInfo) ToMultiNode() MultiNodeInfo {
return MultiNodeInfo{
Children: MultiNode{n.ID},
Parents: MultiNode{n.ParentID},
Timestamps: []uint64{n.Meta.Time},
Meta: n.Meta.Items,
}
}
func isInternal(m []KeyValue) bool {
return len(m) == 1 && m[0].Key == AttributeFilename
}
func mergeNodeInfos(ns []NodeInfo) []MultiNodeInfo {
var r []MultiNodeInfo
for _, info := range ns {
if len(r) == 0 || !r[len(r)-1].Add(info) {
r = append(r, info.ToMultiNode())
}
}
return r
}

View file

@ -0,0 +1,155 @@
package pilorama
import (
"context"
"strings"
"testing"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
"github.com/stretchr/testify/require"
)
func TestDuplicateDirectory(t *testing.T) {
for i := range providers {
if providers[i].name == "inmemory" {
continue
}
t.Run(providers[i].name, func(t *testing.T) {
testDuplicateDirectory(t, providers[i].construct(t))
})
}
}
func testDuplicateDirectory(t *testing.T, f Forest) {
ctx := context.Background()
d := CIDDescriptor{CID: cidtest.ID(), Size: 1}
treeID := "sometree"
treeApply := func(t *testing.T, parent, child uint64, filename string, internal bool) {
// Nothing magic here, we add items in order and children are unique.
// This simplifies function interface a bit.
ts := child
kv := []KeyValue{{Key: AttributeFilename, Value: []byte(filename)}}
if !internal {
kv = append(kv, KeyValue{Key: "uniqueAttr", Value: []byte{byte(child)}})
}
err := f.TreeApply(ctx, d.CID, treeID, &Move{
Parent: parent,
Child: child,
Meta: Meta{
Time: ts,
Items: kv,
},
}, true)
require.NoError(t, err)
}
// The following tree is constructed:
// 0
// [1] |-- dir1 (internal)
// [2] |-- value1
// [3] |-- dir3 (internal)
// [4] |-- value3
// [5] |-- dir1 (internal)
// [6] |-- value2
// [7] |-- dir3 (internal)
// [8] |-- value4
// [9] |-- dir2 (internal)
// [10] |-- value0
treeApply(t, RootID, 1, "dir1", true)
treeApply(t, 1, 2, "value1", false)
treeApply(t, 1, 3, "dir3", true)
treeApply(t, 3, 4, "value3", false)
treeApply(t, RootID, 5, "dir1", true)
treeApply(t, 5, 6, "value2", false)
treeApply(t, 5, 7, "dir3", true)
treeApply(t, 7, 8, "value4", false)
treeApply(t, RootID, 9, "dir2", true)
treeApply(t, RootID, 10, "value0", false)
// The compacted view:
// 0
// [1,5] |-- dir1 (internal)
// [2] |-- value1
// [3,7] |-- dir3 (internal)
// [4] |-- value3
// [8] |-- value4
// [6] |-- value2
// [9] |-- dir2 (internal)
// [10] |-- value0
testGetByPath := func(t *testing.T, p string) []byte {
pp := strings.Split(p, "/")
nodes, err := f.TreeGetByPath(context.Background(), d.CID, treeID, AttributeFilename, pp, false)
require.NoError(t, err)
require.Equal(t, 1, len(nodes))
meta, _, err := f.TreeGetMeta(ctx, d.CID, treeID, nodes[0])
require.NoError(t, err)
require.Equal(t, []byte(pp[len(pp)-1]), meta.GetAttr(AttributeFilename))
return meta.GetAttr("uniqueAttr")
}
require.Equal(t, []byte{2}, testGetByPath(t, "dir1/value1"))
require.Equal(t, []byte{4}, testGetByPath(t, "dir1/dir3/value3"))
require.Equal(t, []byte{8}, testGetByPath(t, "dir1/dir3/value4"))
require.Equal(t, []byte{10}, testGetByPath(t, "value0"))
testSortedByFilename := func(t *testing.T, root MultiNode, last *string, batchSize int) ([]MultiNodeInfo, *string) {
res, last, err := f.TreeSortedByFilename(context.Background(), d.CID, treeID, root, last, batchSize)
require.NoError(t, err)
return res, last
}
t.Run("test sorted listing, full children branch", func(t *testing.T) {
t.Run("big batch size", func(t *testing.T) {
res, _ := testSortedByFilename(t, MultiNode{RootID}, nil, 10)
require.Equal(t, 3, len(res))
require.Equal(t, MultiNode{1, 5}, res[0].Children)
require.Equal(t, MultiNode{9}, res[1].Children)
require.Equal(t, MultiNode{10}, res[2].Children)
t.Run("multi-root", func(t *testing.T) {
res, _ := testSortedByFilename(t, MultiNode{1, 5}, nil, 10)
require.Equal(t, 3, len(res))
require.Equal(t, MultiNode{3, 7}, res[0].Children)
require.Equal(t, MultiNode{2}, res[1].Children)
require.Equal(t, MultiNode{6}, res[2].Children)
})
})
t.Run("small batch size", func(t *testing.T) {
res, last := testSortedByFilename(t, MultiNode{RootID}, nil, 1)
require.Equal(t, 1, len(res))
require.Equal(t, MultiNode{1, 5}, res[0].Children)
res, last = testSortedByFilename(t, MultiNode{RootID}, last, 1)
require.Equal(t, 1, len(res))
require.Equal(t, MultiNode{9}, res[0].Children)
res, last = testSortedByFilename(t, MultiNode{RootID}, last, 1)
require.Equal(t, 1, len(res))
require.Equal(t, MultiNode{10}, res[0].Children)
res, _ = testSortedByFilename(t, MultiNode{RootID}, last, 1)
require.Equal(t, 0, len(res))
t.Run("multi-root", func(t *testing.T) {
res, last := testSortedByFilename(t, MultiNode{1, 5}, nil, 1)
require.Equal(t, 1, len(res))
require.Equal(t, MultiNode{3, 7}, res[0].Children)
res, last = testSortedByFilename(t, MultiNode{1, 5}, last, 1)
require.Equal(t, 1, len(res))
require.Equal(t, MultiNode{2}, res[0].Children)
res, last = testSortedByFilename(t, MultiNode{1, 5}, last, 1)
require.Equal(t, 1, len(res))
require.Equal(t, MultiNode{6}, res[0].Children)
res, _ = testSortedByFilename(t, MultiNode{RootID}, last, 1)
require.Equal(t, 0, len(res))
})
})
})
}

View file

@ -184,13 +184,12 @@ func (s *Shard) TreeGetChildren(ctx context.Context, cid cidSDK.ID, treeID strin
} }
// TreeSortedByFilename implements the pilorama.Forest interface. // TreeSortedByFilename implements the pilorama.Forest interface.
func (s *Shard) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID pilorama.Node, last *string, count int) ([]pilorama.NodeInfo, *string, error) { func (s *Shard) TreeSortedByFilename(ctx context.Context, cid cidSDK.ID, treeID string, nodeID pilorama.MultiNode, last *string, count int) ([]pilorama.MultiNodeInfo, *string, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "Shard.TreeSortedByFilename", ctx, span := tracing.StartSpanFromContext(ctx, "Shard.TreeSortedByFilename",
trace.WithAttributes( trace.WithAttributes(
attribute.String("shard_id", s.ID().String()), attribute.String("shard_id", s.ID().String()),
attribute.String("container_id", cid.EncodeToString()), attribute.String("container_id", cid.EncodeToString()),
attribute.String("tree_id", treeID), attribute.String("tree_id", treeID),
attribute.String("node_id", strconv.FormatUint(nodeID, 10)),
), ),
) )
defer span.End() defer span.End()

View file

@ -6,15 +6,17 @@ import "pkg/services/control/ir/types.proto";
option go_package = "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/ir/control"; option go_package = "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/ir/control";
// `ControlService` provides an interface for internal work with the Inner Ring node. // `ControlService` provides an interface for internal work with the Inner Ring
// node.
service ControlService { service ControlService {
// Performs health check of the IR node. // Performs health check of the IR node.
rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse); rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
// Forces a new epoch to be signaled by the IR node with high probability. // Forces a new epoch to be signaled by the IR node with high probability.
rpc TickEpoch (TickEpochRequest) returns (TickEpochResponse); rpc TickEpoch(TickEpochRequest) returns (TickEpochResponse);
// Forces a node removal to be signaled by the IR node with high probability. // Forces a node removal to be signaled by the IR node with high probability.
rpc RemoveNode (RemoveNodeRequest) returns (RemoveNodeResponse); rpc RemoveNode(RemoveNodeRequest) returns (RemoveNodeResponse);
// Forces a container removal to be signaled by the IR node with high probability. // Forces a container removal to be signaled by the IR node with high
// probability.
rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse); rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse);
} }
@ -48,7 +50,7 @@ message HealthCheckResponse {
} }
message TickEpochRequest { message TickEpochRequest {
message Body{ message Body {
// Valid until block value override. // Valid until block value override.
uint32 vub = 1; uint32 vub = 1;
} }
@ -58,7 +60,7 @@ message TickEpochRequest {
} }
message TickEpochResponse { message TickEpochResponse {
message Body{ message Body {
// Valid until block value for transaction. // Valid until block value for transaction.
uint32 vub = 1; uint32 vub = 1;
} }
@ -68,7 +70,7 @@ message TickEpochResponse {
} }
message RemoveNodeRequest { message RemoveNodeRequest {
message Body{ message Body {
bytes key = 1; bytes key = 1;
// Valid until block value override. // Valid until block value override.
uint32 vub = 2; uint32 vub = 2;
@ -79,7 +81,7 @@ message RemoveNodeRequest {
} }
message RemoveNodeResponse { message RemoveNodeResponse {
message Body{ message Body {
// Valid until block value for transaction. // Valid until block value for transaction.
uint32 vub = 1; uint32 vub = 1;
} }
@ -89,7 +91,7 @@ message RemoveNodeResponse {
} }
message RemoveContainerRequest { message RemoveContainerRequest {
message Body{ message Body {
bytes container_id = 1; bytes container_id = 1;
bytes owner = 2; bytes owner = 2;
// Valid until block value override. // Valid until block value override.
@ -101,7 +103,7 @@ message RemoveContainerRequest {
} }
message RemoveContainerResponse { message RemoveContainerResponse {
message Body{ message Body {
// Valid until block value for transaction. // Valid until block value for transaction.
uint32 vub = 1; uint32 vub = 1;
} }

View file

@ -7,10 +7,10 @@ option go_package = "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/ir/
// Signature of some message. // Signature of some message.
message Signature { message Signature {
// Public key used for signing. // Public key used for signing.
bytes key = 1 [json_name = "key"]; bytes key = 1 [ json_name = "key" ];
// Binary signature. // Binary signature.
bytes sign = 2 [json_name = "signature"]; bytes sign = 2 [ json_name = "signature" ];
} }
// Health status of the IR application. // Health status of the IR application.

View file

@ -48,7 +48,7 @@ func TestGetSubTree(t *testing.T) {
acc := subTreeAcc{errIndex: errIndex} acc := subTreeAcc{errIndex: errIndex}
err := getSubTree(context.Background(), &acc, d.CID, &GetSubTreeRequest_Body{ err := getSubTree(context.Background(), &acc, d.CID, &GetSubTreeRequest_Body{
TreeId: treeID, TreeId: treeID,
RootId: rootID, RootId: []uint64{rootID},
Depth: depth, Depth: depth,
}, p) }, p)
if errIndex == -1 { if errIndex == -1 {
@ -58,12 +58,12 @@ func TestGetSubTree(t *testing.T) {
} }
// GetSubTree must return child only after is has returned the parent. // GetSubTree must return child only after is has returned the parent.
require.Equal(t, rootID, acc.seen[0].Body.NodeId) require.Equal(t, rootID, acc.seen[0].Body.NodeId[0])
loop: loop:
for i := 1; i < len(acc.seen); i++ { for i := 1; i < len(acc.seen); i++ {
parent := acc.seen[i].Body.ParentId parent := acc.seen[i].Body.ParentId
for j := 0; j < i; j++ { for j := 0; j < i; j++ {
if acc.seen[j].Body.NodeId == parent { if acc.seen[j].Body.NodeId[0] == parent[0] {
continue loop continue loop
} }
} }
@ -73,16 +73,16 @@ func TestGetSubTree(t *testing.T) {
// GetSubTree must return valid meta. // GetSubTree must return valid meta.
for i := range acc.seen { for i := range acc.seen {
b := acc.seen[i].Body b := acc.seen[i].Body
meta, node, err := p.TreeGetMeta(context.Background(), d.CID, treeID, b.NodeId) meta, node, err := p.TreeGetMeta(context.Background(), d.CID, treeID, b.NodeId[0])
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, node, b.ParentId) require.Equal(t, node, b.ParentId[0])
require.Equal(t, meta.Time, b.Timestamp) require.Equal(t, meta.Time, b.Timestamp[0])
require.Equal(t, metaToProto(meta.Items), b.Meta) require.Equal(t, metaToProto(meta.Items), b.Meta)
} }
ordered := make([]uint64, len(acc.seen)) ordered := make([]uint64, len(acc.seen))
for i := range acc.seen { for i := range acc.seen {
ordered[i] = acc.seen[i].Body.NodeId ordered[i] = acc.seen[i].Body.NodeId[0]
} }
return ordered return ordered
} }
@ -184,7 +184,7 @@ func testGetSubTreeOrderAsc(t *testing.T, p pilorama.ForestStorage) {
} }
found := false found := false
for j := range tree { for j := range tree {
if acc.seen[i].Body.NodeId == tree[j].id { if acc.seen[i].Body.NodeId[0] == tree[j].id {
found = true found = true
paths = append(paths, path.Join(tree[j].path...)) paths = append(paths, path.Join(tree[j].path...))
} }
@ -207,7 +207,7 @@ func testGetSubTreeOrderAsc(t *testing.T, p pilorama.ForestStorage) {
}, p) }, p)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, acc.seen, 1) require.Len(t, acc.seen, 1)
require.Equal(t, uint64(0), acc.seen[0].Body.NodeId) require.Equal(t, uint64(0), acc.seen[0].Body.NodeId[0])
}) })
t.Run("depth=2", func(t *testing.T) { t.Run("depth=2", func(t *testing.T) {
acc := subTreeAcc{errIndex: -1} acc := subTreeAcc{errIndex: -1}
@ -220,15 +220,16 @@ func testGetSubTreeOrderAsc(t *testing.T, p pilorama.ForestStorage) {
}, p) }, p)
require.NoError(t, err) require.NoError(t, err)
require.Len(t, acc.seen, 3) require.Len(t, acc.seen, 3)
require.Equal(t, uint64(0), acc.seen[0].Body.NodeId) require.Equal(t, uint64(0), acc.seen[0].Body.NodeId[0])
require.Equal(t, uint64(0), acc.seen[1].GetBody().GetParentId()) require.Equal(t, uint64(0), acc.seen[1].GetBody().GetParentId()[0])
require.Equal(t, uint64(0), acc.seen[2].GetBody().GetParentId()) require.Equal(t, uint64(0), acc.seen[2].GetBody().GetParentId()[0])
}) })
} }
var ( var (
errSubTreeSend = errors.New("send finished with error") errSubTreeSend = errors.New("send finished with error")
errSubTreeSendAfterError = errors.New("send was invoked after an error occurred") errSubTreeSendAfterError = errors.New("send was invoked after an error occurred")
errInvalidResponse = errors.New("send got invalid response")
) )
type subTreeAcc struct { type subTreeAcc struct {
@ -241,6 +242,16 @@ type subTreeAcc struct {
var _ TreeService_GetSubTreeServer = &subTreeAcc{} var _ TreeService_GetSubTreeServer = &subTreeAcc{}
func (s *subTreeAcc) Send(r *GetSubTreeResponse) error { func (s *subTreeAcc) Send(r *GetSubTreeResponse) error {
b := r.GetBody()
if len(b.GetNodeId()) > 1 {
return errInvalidResponse
}
if len(b.GetParentId()) > 1 {
return errInvalidResponse
}
if len(b.GetTimestamp()) > 1 {
return errInvalidResponse
}
s.seen = append(s.seen, r) s.seen = append(s.seen, r)
if s.errIndex >= 0 { if s.errIndex >= 0 {
if len(s.seen) == s.errIndex+1 { if len(s.seen) == s.errIndex+1 {

View file

@ -16,6 +16,8 @@ import (
netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
// Service represents tree-service capable of working with multiple // Service represents tree-service capable of working with multiple
@ -440,29 +442,50 @@ func (s *Service) GetSubTree(req *GetSubTreeRequest, srv TreeService_GetSubTreeS
return getSubTree(srv.Context(), srv, cid, b, s.forest) return getSubTree(srv.Context(), srv, cid, b, s.forest)
} }
type stackItem struct {
values []pilorama.MultiNodeInfo
parent pilorama.MultiNode
last *string
}
func getSortedSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid cidSDK.ID, b *GetSubTreeRequest_Body, forest pilorama.Forest) error { func getSortedSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid cidSDK.ID, b *GetSubTreeRequest_Body, forest pilorama.Forest) error {
const batchSize = 1000 const batchSize = 1000
type stackItem struct { // For backward compatibility.
values []pilorama.NodeInfo rootIDs := b.GetRootId()
parent pilorama.Node if len(rootIDs) == 0 {
last *string rootIDs = []uint64{0}
} }
// Traverse the tree in a DFS manner. Because we need to support arbitrary depth, // Traverse the tree in a DFS manner. Because we need to support arbitrary depth,
// recursive implementation is not suitable here, so we maintain explicit stack. // recursive implementation is not suitable here, so we maintain explicit stack.
m, p, err := forest.TreeGetMeta(ctx, cid, b.GetTreeId(), b.GetRootId()) var ms []pilorama.KeyValue
var ps []uint64
var ts []uint64
for _, rootID := range rootIDs {
m, p, err := forest.TreeGetMeta(ctx, cid, b.GetTreeId(), rootID)
if err != nil { if err != nil {
return err return err
} }
if ms == nil {
ms = m.Items
} else {
if len(m.Items) != 1 {
return status.Error(codes.InvalidArgument, "multiple non-internal nodes provided")
}
}
ts = append(ts, m.Time)
ps = append(ps, p)
}
stack := []stackItem{{ stack := []stackItem{{
values: []pilorama.NodeInfo{{ values: []pilorama.MultiNodeInfo{{
ID: b.GetRootId(), Children: rootIDs,
Meta: m, Timestamps: ts,
ParentID: p, Meta: ms,
Parents: ps,
}}, }},
parent: p, parent: ps,
}} }}
for { for {
@ -486,30 +509,20 @@ func getSortedSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid
} }
} }
node := stack[len(stack)-1].values[0] node, err := stackPopAndSend(stack, srv)
stack[len(stack)-1].values = stack[len(stack)-1].values[1:]
err = srv.Send(&GetSubTreeResponse{
Body: &GetSubTreeResponse_Body{
NodeId: node.ID,
ParentId: node.ParentID,
Timestamp: node.Meta.Time,
Meta: metaToProto(node.Meta.Items),
},
})
if err != nil { if err != nil {
return err return err
} }
if b.GetDepth() == 0 || uint32(len(stack)) < b.GetDepth() { if b.GetDepth() == 0 || uint32(len(stack)) < b.GetDepth() {
children, last, err := forest.TreeSortedByFilename(ctx, cid, b.GetTreeId(), node.ID, nil, batchSize) children, last, err := forest.TreeSortedByFilename(ctx, cid, b.GetTreeId(), node.Children, nil, batchSize)
if err != nil { if err != nil {
return err return err
} }
if len(children) != 0 { if len(children) != 0 {
stack = append(stack, stackItem{ stack = append(stack, stackItem{
values: children, values: children,
parent: node.ID, parent: node.Children,
last: last, last: last,
}) })
} }
@ -518,19 +531,38 @@ func getSortedSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid
return nil return nil
} }
func stackPopAndSend(stack []stackItem, srv TreeService_GetSubTreeServer) (pilorama.MultiNodeInfo, error) {
node := stack[len(stack)-1].values[0]
stack[len(stack)-1].values = stack[len(stack)-1].values[1:]
return node, srv.Send(&GetSubTreeResponse{
Body: &GetSubTreeResponse_Body{
NodeId: node.Children,
ParentId: node.Parents,
Timestamp: node.Timestamps,
Meta: metaToProto(node.Meta),
},
})
}
func getSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid cidSDK.ID, b *GetSubTreeRequest_Body, forest pilorama.Forest) error { func getSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid cidSDK.ID, b *GetSubTreeRequest_Body, forest pilorama.Forest) error {
if b.GetOrderBy().GetDirection() == GetSubTreeRequest_Body_Order_Asc { if b.GetOrderBy().GetDirection() == GetSubTreeRequest_Body_Order_Asc {
return getSortedSubTree(ctx, srv, cid, b, forest) return getSortedSubTree(ctx, srv, cid, b, forest)
} }
var rootID uint64
if len(b.GetRootId()) > 0 {
rootID = b.GetRootId()[0]
}
// Traverse the tree in a DFS manner. Because we need to support arbitrary depth, // Traverse the tree in a DFS manner. Because we need to support arbitrary depth,
// recursive implementation is not suitable here, so we maintain explicit stack. // recursive implementation is not suitable here, so we maintain explicit stack.
m, p, err := forest.TreeGetMeta(ctx, cid, b.GetTreeId(), b.GetRootId()) m, p, err := forest.TreeGetMeta(ctx, cid, b.GetTreeId(), rootID)
if err != nil { if err != nil {
return err return err
} }
stack := [][]pilorama.NodeInfo{{{ stack := [][]pilorama.NodeInfo{{{
ID: b.GetRootId(), ID: rootID,
Meta: m, Meta: m,
ParentID: p, ParentID: p,
}}} }}}
@ -548,9 +580,9 @@ func getSubTree(ctx context.Context, srv TreeService_GetSubTreeServer, cid cidSD
err = srv.Send(&GetSubTreeResponse{ err = srv.Send(&GetSubTreeResponse{
Body: &GetSubTreeResponse_Body{ Body: &GetSubTreeResponse_Body{
NodeId: node.ID, NodeId: []uint64{node.ID},
ParentId: node.ParentID, ParentId: []uint64{node.ParentID},
Timestamp: node.Meta.Time, Timestamp: []uint64{node.Meta.Time},
Meta: metaToProto(node.Meta.Items), Meta: metaToProto(node.Meta.Items),
}, },
}) })

Binary file not shown.

View file

@ -28,25 +28,25 @@ service TreeService {
// Otherwise, a request is denied. // Otherwise, a request is denied.
// Add adds new node to the tree. Invoked by a client. // Add adds new node to the tree. Invoked by a client.
rpc Add (AddRequest) returns (AddResponse); rpc Add(AddRequest) returns (AddResponse);
// AddByPath adds new node to the tree by path. Invoked by a client. // AddByPath adds new node to the tree by path. Invoked by a client.
rpc AddByPath (AddByPathRequest) returns (AddByPathResponse); rpc AddByPath(AddByPathRequest) returns (AddByPathResponse);
// Remove removes node from the tree. Invoked by a client. // Remove removes node from the tree. Invoked by a client.
rpc Remove (RemoveRequest) returns (RemoveResponse); rpc Remove(RemoveRequest) returns (RemoveResponse);
// Move moves node from one parent to another. Invoked by a client. // Move moves node from one parent to another. Invoked by a client.
rpc Move (MoveRequest) returns (MoveResponse); rpc Move(MoveRequest) returns (MoveResponse);
// GetNodeByPath returns list of IDs corresponding to a specific filepath. // GetNodeByPath returns list of IDs corresponding to a specific filepath.
rpc GetNodeByPath (GetNodeByPathRequest) returns (GetNodeByPathResponse); rpc GetNodeByPath(GetNodeByPathRequest) returns (GetNodeByPathResponse);
// GetSubTree returns tree corresponding to a specific node. // GetSubTree returns tree corresponding to a specific node.
rpc GetSubTree (GetSubTreeRequest) returns (stream GetSubTreeResponse); rpc GetSubTree(GetSubTreeRequest) returns (stream GetSubTreeResponse);
// TreeList return list of the existing trees in the container. // TreeList return list of the existing trees in the container.
rpc TreeList (TreeListRequest) returns (TreeListResponse); rpc TreeList(TreeListRequest) returns (TreeListResponse);
/* Synchronization API */ /* Synchronization API */
// Apply pushes log operation from another node to the current. // Apply pushes log operation from another node to the current.
// The request must be signed by a container node. // The request must be signed by a container node.
rpc Apply (ApplyRequest) returns (ApplyResponse); rpc Apply(ApplyRequest) returns (ApplyResponse);
// GetOpLog returns a stream of logged operations starting from some height. // GetOpLog returns a stream of logged operations starting from some height.
rpc GetOpLog(GetOpLogRequest) returns (stream GetOpLogResponse); rpc GetOpLog(GetOpLogRequest) returns (stream GetOpLogResponse);
// Healthcheck is a dummy rpc to check service availability // Healthcheck is a dummy rpc to check service availability
@ -85,7 +85,6 @@ message AddResponse {
Signature signature = 2; Signature signature = 2;
}; };
message AddByPathRequest { message AddByPathRequest {
message Body { message Body {
// Container ID in V2 format. // Container ID in V2 format.
@ -122,7 +121,6 @@ message AddByPathResponse {
Signature signature = 2; Signature signature = 2;
}; };
message RemoveRequest { message RemoveRequest {
message Body { message Body {
// Container ID in V2 format. // Container ID in V2 format.
@ -142,8 +140,7 @@ message RemoveRequest {
} }
message RemoveResponse { message RemoveResponse {
message Body { message Body {}
}
// Response body. // Response body.
Body body = 1; Body body = 1;
@ -151,7 +148,6 @@ message RemoveResponse {
Signature signature = 2; Signature signature = 2;
}; };
message MoveRequest { message MoveRequest {
message Body { message Body {
// TODO import neo.fs.v2.refs.ContainerID directly. // TODO import neo.fs.v2.refs.ContainerID directly.
@ -176,8 +172,7 @@ message MoveRequest {
} }
message MoveResponse { message MoveResponse {
message Body { message Body {}
}
// Response body. // Response body.
Body body = 1; Body body = 1;
@ -185,7 +180,6 @@ message MoveResponse {
Signature signature = 2; Signature signature = 2;
}; };
message GetNodeByPathRequest { message GetNodeByPathRequest {
message Body { message Body {
// Container ID in V2 format. // Container ID in V2 format.
@ -235,7 +229,6 @@ message GetNodeByPathResponse {
Signature signature = 2; Signature signature = 2;
}; };
message GetSubTreeRequest { message GetSubTreeRequest {
message Body { message Body {
message Order { message Order {
@ -249,8 +242,8 @@ message GetSubTreeRequest {
bytes container_id = 1; bytes container_id = 1;
// The name of the tree. // The name of the tree.
string tree_id = 2; string tree_id = 2;
// ID of the root node of a subtree. // IDs of the root nodes of a subtree forest.
uint64 root_id = 3; repeated uint64 root_id = 3 [ packed = false ];
// Optional depth of the traversal. Zero means return only root. // Optional depth of the traversal. Zero means return only root.
// Maximum depth is 10. // Maximum depth is 10.
uint32 depth = 4; uint32 depth = 4;
@ -269,11 +262,11 @@ message GetSubTreeRequest {
message GetSubTreeResponse { message GetSubTreeResponse {
message Body { message Body {
// ID of the node. // ID of the node.
uint64 node_id = 1; repeated uint64 node_id = 1 [ packed = false ];
// ID of the parent. // ID of the parent.
uint64 parent_id = 2; repeated uint64 parent_id = 2 [ packed = false ];
// Time node was first added to a tree. // Time node was first added to a tree.
uint64 timestamp = 3; repeated uint64 timestamp = 3 [ packed = false ];
// Node meta-information. // Node meta-information.
repeated KeyValue meta = 4; repeated KeyValue meta = 4;
} }
@ -307,7 +300,6 @@ message TreeListResponse {
Signature signature = 2; Signature signature = 2;
} }
message ApplyRequest { message ApplyRequest {
message Body { message Body {
// Container ID in V2 format. // Container ID in V2 format.
@ -325,8 +317,7 @@ message ApplyRequest {
} }
message ApplyResponse { message ApplyResponse {
message Body { message Body {}
}
// Response body. // Response body.
Body body = 1; Body body = 1;
@ -334,7 +325,6 @@ message ApplyResponse {
Signature signature = 2; Signature signature = 2;
}; };
message GetOpLogRequest { message GetOpLogRequest {
message Body { message Body {
// Container ID in V2 format. // Container ID in V2 format.
@ -366,8 +356,7 @@ message GetOpLogResponse {
}; };
message HealthcheckResponse { message HealthcheckResponse {
message Body { message Body {}
}
// Response body. // Response body.
Body body = 1; Body body = 1;
@ -376,8 +365,7 @@ message HealthcheckResponse {
}; };
message HealthcheckRequest { message HealthcheckRequest {
message Body { message Body {}
}
// Request body. // Request body.
Body body = 1; Body body = 1;

Binary file not shown.

Binary file not shown.

View file

@ -10,25 +10,25 @@ option go_package = "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/tre
// KeyValue represents key-value pair attached to an object. // KeyValue represents key-value pair attached to an object.
message KeyValue { message KeyValue {
// Attribute name. // Attribute name.
string key = 1 [json_name = "key"]; string key = 1 [ json_name = "key" ];
// Attribute value. // Attribute value.
bytes value = 2 [json_name = "value"]; bytes value = 2 [ json_name = "value" ];
} }
// LogMove represents log-entry for a single move operation. // LogMove represents log-entry for a single move operation.
message LogMove { message LogMove {
// ID of the parent node. // ID of the parent node.
uint64 parent_id = 1 [json_name = "parentID"]; uint64 parent_id = 1 [ json_name = "parentID" ];
// Node meta information, including operation timestamp. // Node meta information, including operation timestamp.
bytes meta = 2 [json_name = "meta"]; bytes meta = 2 [ json_name = "meta" ];
// ID of the node to move. // ID of the node to move.
uint64 child_id = 3 [json_name = "childID"]; uint64 child_id = 3 [ json_name = "childID" ];
} }
// Signature of a message. // Signature of a message.
message Signature { message Signature {
// Serialized public key as defined in FrostFS API. // Serialized public key as defined in FrostFS API.
bytes key = 1 [json_name = "key"]; bytes key = 1 [ json_name = "key" ];
// Signature of a message body. // Signature of a message body.
bytes sign = 2 [json_name = "signature"]; bytes sign = 2 [ json_name = "signature" ];
} }