From 9cbd4271f1b0063b7a61f585ddb90f3f4c4cfdba Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 11 May 2022 16:29:04 +0300 Subject: [PATCH] [#1329] services/tree: Implement `GetOpLog` RPC Signed-off-by: Evgenii Stratonikov --- pkg/local_object_storage/engine/tree.go | 19 ++++++ pkg/local_object_storage/pilorama/boltdb.go | 39 +++++++++++- pkg/local_object_storage/pilorama/forest.go | 19 ++++++ .../pilorama/forest_test.go | 58 ++++++++++++++++++ .../pilorama/interface.go | 3 + pkg/local_object_storage/shard/tree.go | 5 ++ pkg/services/tree/service.go | 32 ++++++++++ pkg/services/tree/service.pb.go | Bin 86587 -> 97443 bytes pkg/services/tree/service.proto | 23 +++++++ pkg/services/tree/service_grpc.pb.go | Bin 13171 -> 15010 bytes pkg/services/tree/service_neofs.pb.go | Bin 41371 -> 46953 bytes 11 files changed, 195 insertions(+), 3 deletions(-) diff --git a/pkg/local_object_storage/engine/tree.go b/pkg/local_object_storage/engine/tree.go index cc15853f4..a800b75b6 100644 --- a/pkg/local_object_storage/engine/tree.go +++ b/pkg/local_object_storage/engine/tree.go @@ -116,3 +116,22 @@ func (e *StorageEngine) TreeGetChildren(cid cidSDK.ID, treeID string, nodeID pil } return nil, err } + +// TreeGetOpLog implements the pilorama.Forest interface. +func (e *StorageEngine) TreeGetOpLog(cid cidSDK.ID, treeID string, height uint64) (pilorama.Move, error) { + var err error + var lm pilorama.Move + for _, sh := range e.sortShardsByWeight(cid) { + lm, err = sh.TreeGetOpLog(cid, treeID, height) + if err != nil { + e.log.Debug("can't perform `GetOpLog`", + zap.Stringer("cid", cid), + zap.String("tree", treeID), + zap.Uint64("height", height), + zap.String("err", err.Error())) + continue + } + return lm, nil + } + return lm, err +} diff --git a/pkg/local_object_storage/pilorama/boltdb.go b/pkg/local_object_storage/pilorama/boltdb.go index 4644786e1..18becc445 100644 --- a/pkg/local_object_storage/pilorama/boltdb.go +++ b/pkg/local_object_storage/pilorama/boltdb.go @@ -211,7 +211,7 @@ func (t *boltForest) applyOperation(logBucket, treeBucket *bbolt.Bucket, m *Move // 1. Undo up until the desired timestamp is here. for len(key) == 8 && binary.BigEndian.Uint64(key) > m.Time { - if err := t.logFromBytes(&tmp, key, value); err != nil { + if err := t.logFromBytes(&tmp, value); err != nil { return nil, err } if err := t.undo(&tmp.Move, &tmp, treeBucket, cKey[:]); err != nil { @@ -231,7 +231,7 @@ func (t *boltForest) applyOperation(logBucket, treeBucket *bbolt.Bucket, m *Move // 3. Re-apply all other operations. for len(key) == 8 { - if err := t.logFromBytes(&tmp, key, value); err != nil { + if err := t.logFromBytes(&tmp, value); err != nil { return nil, err } if err := t.do(logBucket, treeBucket, cKey[:], &tmp); err != nil { @@ -446,6 +446,29 @@ func (t *boltForest) TreeGetChildren(cid cidSDK.ID, treeID string, nodeID Node) return children, err } +// TreeGetOpLog implements the pilorama.Forest interface. +func (t *boltForest) TreeGetOpLog(cid cidSDK.ID, treeID string, height uint64) (Move, error) { + key := make([]byte, 8) + binary.BigEndian.PutUint64(key, height) + + var lm Move + + err := t.db.View(func(tx *bbolt.Tx) error { + treeRoot := tx.Bucket(bucketName(cid, treeID)) + if treeRoot == nil { + return ErrTreeNotFound + } + + c := treeRoot.Bucket(logBucket).Cursor() + if _, data := c.Seek(key); data != nil { + return t.moveFromBytes(&lm, data) + } + return nil + }) + + return lm, err +} + func (t *boltForest) getPathPrefix(bTree *bbolt.Bucket, attr string, path []string) (int, Node, error) { var key [9]byte @@ -483,7 +506,17 @@ loop: return len(path), curNode, nil } -func (t *boltForest) logFromBytes(lm *LogMove, key []byte, data []byte) error { +func (t *boltForest) moveFromBytes(m *Move, data []byte) error { + r := io.NewBinReaderFromBuf(data) + m.Child = r.ReadU64LE() + m.Parent = r.ReadU64LE() + if err := m.Meta.FromBytes(r.ReadVarBytes()); err != nil { + return err + } + return r.Err +} + +func (t *boltForest) logFromBytes(lm *LogMove, data []byte) error { r := io.NewBinReaderFromBuf(data) lm.Child = r.ReadU64LE() lm.Parent = r.ReadU64LE() diff --git a/pkg/local_object_storage/pilorama/forest.go b/pkg/local_object_storage/pilorama/forest.go index 255ca9ea3..174bb529e 100644 --- a/pkg/local_object_storage/pilorama/forest.go +++ b/pkg/local_object_storage/pilorama/forest.go @@ -1,6 +1,8 @@ package pilorama import ( + "sort" + cidSDK "github.com/nspcc-dev/neofs-sdk-go/container/id" ) @@ -133,3 +135,20 @@ func (f *memoryForest) TreeGetChildren(cid cidSDK.ID, treeID string, nodeID Node copy(res, children) return res, nil } + +// TreeGetOpLog implements the pilorama.Forest interface. +func (f *memoryForest) TreeGetOpLog(cid cidSDK.ID, treeID string, height uint64) (Move, error) { + fullID := cid.String() + "/" + treeID + s, ok := f.treeMap[fullID] + if !ok { + return Move{}, ErrTreeNotFound + } + + n := sort.Search(len(s.operations), func(i int) bool { + return s.operations[i].Time >= height + }) + if n == len(s.operations) { + return Move{}, nil + } + return s.operations[n].Move, nil +} diff --git a/pkg/local_object_storage/pilorama/forest_test.go b/pkg/local_object_storage/pilorama/forest_test.go index 4b80a5d4f..6c9077f0e 100644 --- a/pkg/local_object_storage/pilorama/forest_test.go +++ b/pkg/local_object_storage/pilorama/forest_test.go @@ -324,6 +324,64 @@ func testForestTreeApply(t *testing.T, constructor func(t *testing.T) Forest) { }) } +func TestForest_GetOpLog(t *testing.T) { + for i := range providers { + t.Run(providers[i].name, func(t *testing.T) { + testForestTreeGetOpLog(t, providers[i].construct) + }) + } +} + +func testForestTreeGetOpLog(t *testing.T, constructor func(t *testing.T) Forest) { + cid := cidtest.ID() + treeID := "version" + logs := []Move{ + { + Meta: Meta{Time: 4, Items: []KeyValue{{"grand", []byte{1}}}}, + Child: 1, + }, + { + Meta: Meta{Time: 5, Items: []KeyValue{{"second", []byte{1, 2, 3}}}}, + Child: 4, + }, + { + Parent: 10, + Meta: Meta{Time: 256 + 4, Items: []KeyValue{}}, // make sure keys are big-endian + Child: 11, + }, + } + + s := constructor(t) + + t.Run("empty log, no panic", func(t *testing.T) { + _, err := s.TreeGetOpLog(cid, treeID, 0) + require.ErrorIs(t, err, ErrTreeNotFound) + }) + + for i := range logs { + require.NoError(t, s.TreeApply(cid, treeID, &logs[i])) + } + + testGetOpLog := func(t *testing.T, height uint64, m Move) { + lm, err := s.TreeGetOpLog(cid, treeID, height) + require.NoError(t, err) + require.Equal(t, m, lm) + } + + testGetOpLog(t, 0, logs[0]) + testGetOpLog(t, 4, logs[0]) + testGetOpLog(t, 5, logs[1]) + testGetOpLog(t, 6, logs[2]) + testGetOpLog(t, 260, logs[2]) + t.Run("missing entry", func(t *testing.T) { + testGetOpLog(t, 261, Move{}) + }) + t.Run("missing tree", func(t *testing.T) { + _, err := s.TreeGetOpLog(cid, treeID+"123", 4) + require.ErrorIs(t, err, ErrTreeNotFound) + }) +} + func TestForest_ApplyRandom(t *testing.T) { for i := range providers { t.Run(providers[i].name, func(t *testing.T) { diff --git a/pkg/local_object_storage/pilorama/interface.go b/pkg/local_object_storage/pilorama/interface.go index 2a4054813..9dc9ca7dc 100644 --- a/pkg/local_object_storage/pilorama/interface.go +++ b/pkg/local_object_storage/pilorama/interface.go @@ -26,6 +26,9 @@ type Forest interface { // TreeGetChildren returns children of the node with the specified ID. The order is arbitrary. // Should return ErrTreeNotFound if the tree is not found, and empty result if the node is not in the tree. TreeGetChildren(cid cidSDK.ID, treeID string, nodeID Node) ([]uint64, error) + // 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. + TreeGetOpLog(cid cidSDK.ID, treeID string, height uint64) (Move, error) } type ForestStorage interface { diff --git a/pkg/local_object_storage/shard/tree.go b/pkg/local_object_storage/shard/tree.go index d6eefb7be..36e62220a 100644 --- a/pkg/local_object_storage/shard/tree.go +++ b/pkg/local_object_storage/shard/tree.go @@ -36,3 +36,8 @@ func (s *Shard) TreeGetMeta(cid cidSDK.ID, treeID string, nodeID pilorama.Node) func (s *Shard) TreeGetChildren(cid cidSDK.ID, treeID string, nodeID pilorama.Node) ([]uint64, error) { return s.pilorama.TreeGetChildren(cid, treeID, nodeID) } + +// TreeGetOpLog implements the pilorama.Forest interface. +func (s *Shard) TreeGetOpLog(cid cidSDK.ID, treeID string, height uint64) (pilorama.Move, error) { + return s.pilorama.TreeGetOpLog(cid, treeID, height) +} diff --git a/pkg/services/tree/service.go b/pkg/services/tree/service.go index c9b2e1a97..693d36fb3 100644 --- a/pkg/services/tree/service.go +++ b/pkg/services/tree/service.go @@ -342,6 +342,38 @@ loop: }) } +func (s *Service) GetOpLog(req *GetOpLogRequest, srv TreeService_GetOpLogServer) error { + b := req.GetBody() + + var cid cidSDK.ID + if err := cid.Decode(req.GetBody().GetContainerId()); err != nil { + return err + } + + h := b.GetHeight() + for { + lm, err := s.forest.TreeGetOpLog(cid, b.GetTreeId(), h) + if err != nil || lm.Time == 0 { + return err + } + + err = srv.Send(&GetOpLogResponse{ + Body: &GetOpLogResponse_Body{ + Operation: &LogMove{ + ParentId: lm.Parent, + Meta: lm.Meta.Bytes(), + ChildId: lm.Child, + }, + }, + }) + if err != nil { + return err + } + + h = lm.Time + 1 + } +} + func protoToMeta(arr []*KeyValue) []pilorama.KeyValue { meta := make([]pilorama.KeyValue, len(arr)) for i, kv := range arr { diff --git a/pkg/services/tree/service.pb.go b/pkg/services/tree/service.pb.go index 50027db5ce42316405ac22fd02517cc3a3239e9a..77947522f063d5b2461aff3b9fd03b65cf59f3ba 100644 GIT binary patch delta 4498 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 EA0l()P9Re7ZDQ4S z>uGC6p0+a?p_A0ZzO1bvD+YslTZ+?3EPx5@8 z=lOn~*YEfD>VI|byrz5ebSx>Ys7=E+TH<)I{X3lf>1Axto06va4{Aa%n_w;^)A9q~ABD(?WvQ7By{>3<&WyQ??bd+A|NZQuD zq{XS#UAW?&IT+lOj_>ZM#NPFXv8wAz^5%v?EoS$w#)fS+{I0zc+kTXRH@EW1T{>L3 z&z{mtyO!g|zRBd+)@Rf3$e?gG;46803_dB?E@RGy$>h$bj%aakOB@e8o1J2s-gy8& zAG(Y;^UcYjX98Lb?_i_Lfg7+j~0LVICbU2R%9tU1O#2%0%m^Ica zRq)v#^mt~C9j_M8!zb2T@XY#$@#?;7!e~ww9zLhV(a|n+R2oN)SyFkAA3e+}EzbYl zS9l|A#J)d@o;s7mZ+7eOt=ENbCMHf^L)RNul99iBp&dCI$El`t96miYa#V{w$1Wv1 z&p6WXKPOhRJ_F}`p!PiY-YBjbn?yhB79DnBZ_3u6{Qm4Sjb=r5nvBxMK!?*o*B3*p zK_FS6k|tFXw^@H%?m6dfrIy>cB;vOTdOdT=dWlfOPuPk~>bB zc;CfrTrv9~Zo844qIC<}3MaH`Oh!C3>Bp;=)9D{ha4@E`g+P>}%< znkWFP)_;dFi;m8Pd>VCvhjP7Op`9|b_bZS?<1!cYXd!qhsDM*2bj)Yh%p%CMdt|3W z+Ndbv*UM$G>}FC5dcn>iTfML>m8uHgRv~WtyamV4SwzYvgD*uP`Ed5!17k1xpiGk{ z2Nu(IKNQf)Enpfm2Vid+-B$_=Ik)Ok2x`0~)Lseq&}b&8) z5glzWhayk}uBUi8*vT3JGvx-MfLpOQ2yTFY!xK`2a<{UsEuBT#bN zOA)E((uN4+rMp!xI#>(y=wt*6j2u37&`Zagcv^LhJX3}H)Wi)KZsZ%G6bmqy&NhOi zQT!^%i3&@A);05)S7N+5@hJCPY2=1X3wR>R7qq{TqXe5lM>nGIfI*Gq;S@!r-!$Ze%ko$`H4d zOK~a9~#p z)pd$5jL~qiBEKY*@5t{CW%ctPhq7V?6rijY0V^o$sDP~;l5Ls_L8-}Q8k!0kT5#)B z1Yz2|1yeR36PnB70(Un;Sx{MG z^`UA$DvC@l(+-w}SdpuvpjLu#BFu%8UummD)l2BWqQXYU5-L`sV++v_^GR@OUW!Jp d0wR=d>6mZM*T2X-`HT@P4&E54!i+SI1^^Z$sxANk delta 47 zcmV+~0MP%Ub@OJh@&~hH2-FI*i58s%27L+&FE1dI@FE_wh#RE^v*IB`60 zrZd77iEO^WY{Cdr$(W@$`KG%tD_G%V#rQc8b({4(#h5tUQ%k@`Z%*Ktg5XWExC@t= ze9$is!TFGS5^9Yguf^m9Psz<8eRCNR@`WuMCJUHpO?IES2rjt!qPjT3?9CrFry_Wh S`*rTY6;Aed%13YvO6>ru(qO&- delta 9 RcmaF)j%oH`rVX