Skip to content

Commit ed199a8

Browse files
committed
meta: support big objects
Support chained objects in meta-service. Refs #3189, #3138. Signed-off-by: Pavel Karpy <[email protected]>
1 parent 7d71ae0 commit ed199a8

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

pkg/services/meta/containers.go

+120
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ func deleteObjectsOps(dbKV map[string][]byte, s storage.Store, objects []byte, c
299299
rng := storage.SeekRange{}
300300
mptKV := make(map[string][]byte)
301301

302+
objects = expandChildren(s, objects)
303+
302304
// nil value means "delete" operation
303305

304306
for len(objects) > 0 {
@@ -591,3 +593,121 @@ func (s *containerStorage) handleNewEpoch(e uint64) error {
591593

592594
return errors.Join(mptErr, dbErr)
593595
}
596+
597+
func expandChildren(st storage.Store, rootOIDs []byte) []byte {
598+
if len(rootOIDs) == 0 {
599+
return nil
600+
}
601+
602+
// sorting before every SEEK is used for exact single operation for every
603+
// children searching subroutine; search is done in 3 steps:
604+
// 1. root -> last/link object search
605+
// 2. last/link -> first part ID search
606+
// 3. first part ID -> all children search
607+
608+
var childrenWithParentID [][]byte
609+
var rng storage.SeekRange
610+
rng.Prefix = slices.Concat([]byte{attrPlainToOIDIndex}, []byte(objectsdk.FilterParentID), object.AttributeDelimiter)
611+
rng.Start = make([]byte, oid.Size)
612+
613+
rootsSorted := slices.SortedFunc(slices.Chunk(rootOIDs, oid.Size), bytes.Compare)
614+
lastRoot := rootsSorted[len(rootsSorted)-1]
615+
copy(rng.Start, rootsSorted[0])
616+
617+
st.Seek(rng, func(k, _ []byte) bool {
618+
currRoot := k[len(rng.Prefix) : len(rng.Prefix)+oid.Size]
619+
if len(rootsSorted) == 0 || bytes.Compare(currRoot, lastRoot) > 0 {
620+
return false
621+
}
622+
for i := 0; i < len(rootsSorted); i++ {
623+
switch bytes.Compare(currRoot, rootsSorted[i]) {
624+
case -1:
625+
return true
626+
case +1:
627+
rootsSorted = rootsSorted[i+1:]
628+
i = -1
629+
continue
630+
case 0:
631+
childrenWithParentID = append(childrenWithParentID, slices.Clone(k[len(rng.Prefix)+oid.Size+1:]))
632+
rootsSorted = rootsSorted[i+1:]
633+
return true
634+
}
635+
}
636+
637+
return true
638+
})
639+
640+
if len(childrenWithParentID) == 0 {
641+
// all objects are roots, no additional children
642+
return rootOIDs
643+
}
644+
645+
firstPartOIDs := make([][]byte, 0, len(childrenWithParentID))
646+
rng.Prefix = []byte{oidToAttrIndex}
647+
copy(rng.Start, childrenWithParentID[0])
648+
649+
slices.SortFunc(childrenWithParentID, bytes.Compare)
650+
st.Seek(rng, func(k, _ []byte) bool {
651+
if len(childrenWithParentID) == 0 {
652+
return false
653+
}
654+
if !bytes.HasPrefix(k[1+oid.Size:], []byte(objectsdk.FilterFirstSplitObject)) {
655+
return true
656+
}
657+
currChild := k[1 : 1+oid.Size]
658+
for i := 0; i < len(childrenWithParentID); i++ {
659+
switch bytes.Compare(currChild, childrenWithParentID[i]) {
660+
case -1:
661+
return true
662+
case +1:
663+
// should never happen, it means we have info
664+
// about child object with parent ID but child
665+
// object does not have first part ID, nothing
666+
// can be done, just skip
667+
childrenWithParentID = childrenWithParentID[i+1:]
668+
i = -1
669+
continue
670+
case 0:
671+
firstPartOIDs = append(firstPartOIDs, k[1+oid.Size+len(objectsdk.FilterFirstSplitObject)+1:])
672+
childrenWithParentID = childrenWithParentID[i+1:]
673+
return true
674+
}
675+
}
676+
677+
return true
678+
})
679+
680+
if len(firstPartOIDs) == 0 {
681+
// unexpected but nothing to do
682+
return rootOIDs
683+
}
684+
685+
children := slices.Concat(firstPartOIDs...) // first objects are children too
686+
rng.Prefix = slices.Concat([]byte{attrPlainToOIDIndex}, []byte(objectsdk.FilterFirstSplitObject), object.AttributeDelimiter)
687+
copy(rng.Start, firstPartOIDs[0])
688+
689+
slices.SortFunc(firstPartOIDs, bytes.Compare)
690+
st.Seek(rng, func(k, _ []byte) bool {
691+
currChild := k[len(rng.Prefix) : len(rng.Prefix)+oid.Size]
692+
if len(firstPartOIDs) == 0 {
693+
return false
694+
}
695+
for i := 0; i < len(firstPartOIDs); i++ {
696+
switch bytes.Compare(currChild, firstPartOIDs[i]) {
697+
case -1:
698+
return true
699+
case +1:
700+
firstPartOIDs = firstPartOIDs[i+1:]
701+
i = -1
702+
continue
703+
case 0:
704+
children = append(children, slices.Clone(k[len(rng.Prefix)+oid.Size+1:])...)
705+
return true
706+
}
707+
}
708+
709+
return true
710+
})
711+
712+
return slices.Concat(rootOIDs, children)
713+
}

pkg/services/meta/containers_test.go

+142
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package meta
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"math/big"
@@ -41,6 +42,7 @@ func TestObjectExpiration(t *testing.T) {
4142
var oo []object.Object
4243
for i := range objNum {
4344
o := objecttest.Object()
45+
o.ResetRelations()
4446
o.SetContainerID(cID)
4547
setExpiration(&o, uint64(i))
4648

@@ -99,9 +101,11 @@ func TestObjectExpiration(t *testing.T) {
99101

100102
o := objecttest.Object()
101103
oID := o.GetID()
104+
o.ResetRelations()
102105
o.SetContainerID(cID)
103106
setExpiration(&o, objExp)
104107
lock := objecttest.Object()
108+
lock.ResetRelations()
105109
lock.SetContainerID(cID)
106110
setExpiration(&lock, lockExp)
107111

@@ -153,3 +157,141 @@ func TestObjectExpiration(t *testing.T) {
153157
})
154158
})
155159
}
160+
161+
func objectChain(cID cid.ID, length int) (object.Object, []object.Object) {
162+
reset := func(o *object.Object) {
163+
o.SetContainerID(cID)
164+
o.ResetRelations()
165+
o.SetType(object.TypeRegular)
166+
}
167+
168+
root := objecttest.Object()
169+
reset(&root)
170+
first := objecttest.Object()
171+
reset(&first)
172+
173+
children := make([]object.Object, length-1)
174+
children[0] = first
175+
for i := range children {
176+
if i != 0 {
177+
children[i] = objecttest.Object()
178+
reset(&children[i])
179+
children[i].SetFirstID(first.GetID())
180+
children[i].SetPreviousID(children[i-1].GetID())
181+
}
182+
if i == len(children)-1 {
183+
children[i].SetParentID(root.GetID())
184+
children[i].SetParent(&root)
185+
}
186+
}
187+
188+
link := objecttest.Object()
189+
reset(&link)
190+
link.SetFirstID(first.GetID())
191+
link.SetParentID(root.GetID())
192+
link.SetParent(&root)
193+
link.SetType(object.TypeLink)
194+
195+
return root, append(children, link)
196+
}
197+
198+
type bigObj struct {
199+
root object.Object
200+
children []object.Object
201+
}
202+
203+
func TestBigObjects(t *testing.T) {
204+
cID := cidtest.ID()
205+
l := zaptest.NewLogger(t)
206+
ctx := context.Background()
207+
208+
var bigObjs []bigObj
209+
for range 10 {
210+
root, children := objectChain(cID, 10)
211+
bigObjs = append(bigObjs, bigObj{root, children})
212+
}
213+
214+
var rawObjSlice []object.Object
215+
for _, obj := range bigObjs {
216+
rawObjSlice = append(rawObjSlice, obj.children...)
217+
}
218+
219+
net := testNetwork{}
220+
net.setContainers([]cid.ID{cID})
221+
net.setObjects(objsToAddrMap(rawObjSlice))
222+
ee := make([]objEvent, 0, len(rawObjSlice))
223+
for _, o := range rawObjSlice {
224+
ev := objEvent{
225+
cID: cID,
226+
oID: o.GetID(),
227+
size: big.NewInt(testObjectSize),
228+
network: big.NewInt(testNetworkMagic),
229+
}
230+
ev.typ = o.Type()
231+
if id := o.GetPreviousID(); !id.IsZero() {
232+
ev.prevObject = id[:]
233+
}
234+
if id := o.GetFirstID(); !id.IsZero() {
235+
ev.firstObject = id[:]
236+
}
237+
238+
ee = append(ee, ev)
239+
}
240+
241+
st, err := storageForContainer(t.TempDir(), cID)
242+
require.NoError(t, err)
243+
t.Cleanup(func() {
244+
_ = st.drop()
245+
})
246+
247+
st.putObjects(ctx, l, 0, ee, &net)
248+
249+
tsObj := objecttest.Object()
250+
tsObj.ResetRelations()
251+
tsObj.SetContainerID(cID)
252+
tsObj.SetType(object.TypeTombstone)
253+
tsEv := objEvent{
254+
cID: cID,
255+
oID: tsObj.GetID(),
256+
size: big.NewInt(testObjectSize),
257+
network: big.NewInt(testNetworkMagic),
258+
}
259+
net.setObjects(objsToAddrMap([]object.Object{tsObj}))
260+
261+
for _, bigO := range bigObjs {
262+
rID := bigO.root.GetID()
263+
tsEv.deletedObjects = rID[:]
264+
265+
st.putObjects(ctx, l, 0, []objEvent{tsEv}, &net)
266+
267+
k := make([]byte, 1+oid.Size)
268+
k[0] = oidIndex
269+
270+
for _, child := range bigO.children {
271+
chID := child.GetID()
272+
copy(k[1:], chID[:])
273+
274+
_, err = st.db.Get(k)
275+
require.ErrorIs(t, err, storage.ErrKeyNotFound)
276+
}
277+
}
278+
279+
// only last operation from TS should be kept
280+
tsID := tsObj.GetID()
281+
st.db.Seek(storage.SeekRange{}, func(k, _ []byte) bool {
282+
switch k[0] {
283+
case oidIndex, oidToAttrIndex, deletedIndex:
284+
if bytes.Equal(k[1:1+oid.Size], tsID[:]) {
285+
return true
286+
}
287+
case attrIntToOIDIndex, attrPlainToOIDIndex:
288+
if bytes.Equal(k[len(k)-oid.Size:], tsID[:]) {
289+
return true
290+
}
291+
default:
292+
}
293+
294+
t.Fatalf("empty db is expected after full clean, found key: %d", k[0])
295+
return true
296+
})
297+
}

0 commit comments

Comments
 (0)