frostfs-node/pkg/local_object_storage/engine/inhume_test.go
Aleksey Savchuk 77b8545601
[#1689] engine: Fix removal of objects not found on node
Ensured correct object status if the object is not found on a node.
Fixed regression introduced in #1450.

Besides an object not being found on any shard, it also important to
remove it anyway in order to populate the metabase indexes because
they are responsible for the correct object status, i.e., the status
will be `object not found` without the indexes, the status will be
`object is already removed` with the indexes.

Change-Id: I6237fbc0f8bb0c4f2a51ada3a68f52950050e660
Signed-off-by: Aleksey Savchuk <a.savchuk@yadro.com>
2025-04-22 10:04:42 +03:00

342 lines
9.8 KiB
Go

package engine
import (
"context"
"fmt"
"strconv"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/testutil"
meta "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/metabase"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/shard"
objectV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
objecttest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/test"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
)
func TestStorageEngine_Inhume(t *testing.T) {
cnr := cidtest.ID()
splitID := objectSDK.NewSplitID()
fs := objectSDK.SearchFilters{}
fs.AddRootFilter()
tombstoneID := object.AddressOf(testutil.GenerateObjectWithCID(cnr))
parent := testutil.GenerateObjectWithCID(cnr)
child := testutil.GenerateObjectWithCID(cnr)
child.SetParent(parent)
idParent, _ := parent.ID()
child.SetParentID(idParent)
child.SetSplitID(splitID)
link := testutil.GenerateObjectWithCID(cnr)
link.SetParent(parent)
link.SetParentID(idParent)
idChild, _ := child.ID()
link.SetChildren(idChild)
link.SetSplitID(splitID)
t.Run("delete small object", func(t *testing.T) {
t.Parallel()
e := testNewEngine(t).setShardsNum(t, 1).prepare(t).engine
defer func() { require.NoError(t, e.Close(context.Background())) }()
err := Put(context.Background(), e, parent, false)
require.NoError(t, err)
var inhumePrm InhumePrm
inhumePrm.WithTarget(tombstoneID, object.AddressOf(parent))
err = e.Inhume(context.Background(), inhumePrm)
require.NoError(t, err)
addrs, err := Select(context.Background(), e, cnr, false, fs)
require.NoError(t, err)
require.Empty(t, addrs)
})
t.Run("delete big object", func(t *testing.T) {
t.Parallel()
te := testNewEngine(t).setShardsNum(t, 2).prepare(t)
e := te.engine
defer func() { require.NoError(t, e.Close(context.Background())) }()
s1, s2 := te.shards[0], te.shards[1]
var putChild shard.PutPrm
putChild.SetObject(child)
_, err := s1.Put(context.Background(), putChild)
require.NoError(t, err)
var putLink shard.PutPrm
putLink.SetObject(link)
_, err = s2.Put(context.Background(), putLink)
require.NoError(t, err)
var inhumePrm InhumePrm
inhumePrm.WithTarget(tombstoneID, object.AddressOf(parent))
err = e.Inhume(context.Background(), inhumePrm)
require.NoError(t, err)
addrs, err := Select(context.Background(), e, cnr, false, fs)
require.NoError(t, err)
require.Empty(t, addrs)
})
}
func TestStorageEngine_ECInhume(t *testing.T) {
parentObjectAddress := oidtest.Address()
containerID := parentObjectAddress.Container()
chunkObject0 := testutil.GenerateObjectWithCID(containerID)
chunkObject0.SetECHeader(objectSDK.NewECHeader(
objectSDK.ECParentInfo{
ID: parentObjectAddress.Object(),
}, 0, 4, []byte{}, 0))
chunkObject1 := testutil.GenerateObjectWithCID(containerID)
chunkObject1.SetECHeader(objectSDK.NewECHeader(
objectSDK.ECParentInfo{
ID: parentObjectAddress.Object(),
}, 1, 4, []byte{}, 0))
tombstone := objectSDK.NewTombstone()
tombstone.SetMembers([]oid.ID{parentObjectAddress.Object()})
payload, err := tombstone.Marshal()
require.NoError(t, err)
tombstoneObject := testutil.GenerateObjectWithCID(containerID)
tombstoneObject.SetType(objectSDK.TypeTombstone)
tombstoneObject.SetPayload(payload)
tombstoneObjectAddress := object.AddressOf(tombstoneObject)
e := testNewEngine(t).setShardsNum(t, 5).prepare(t).engine
defer func() { require.NoError(t, e.Close(context.Background())) }()
require.NoError(t, Put(context.Background(), e, chunkObject0, false))
require.NoError(t, Put(context.Background(), e, tombstoneObject, false))
var inhumePrm InhumePrm
inhumePrm.WithTarget(tombstoneObjectAddress, parentObjectAddress)
err = e.Inhume(context.Background(), inhumePrm)
require.NoError(t, err)
var alreadyRemoved *apistatus.ObjectAlreadyRemoved
require.ErrorAs(t, Put(context.Background(), e, chunkObject0, false), &alreadyRemoved)
require.ErrorAs(t, Put(context.Background(), e, chunkObject1, false), &alreadyRemoved)
}
func TestInhumeExpiredRegularObject(t *testing.T) {
t.Parallel()
const currEpoch = 42
const objectExpiresAfter = currEpoch - 1
engine := testNewEngine(t).setShardsNumAdditionalOpts(t, 1, func(_ int) []shard.Option {
return []shard.Option{
shard.WithDisabledGC(),
shard.WithMetaBaseOptions(append(
testGetDefaultMetabaseOptions(t),
meta.WithEpochState(epochState{currEpoch}),
)...),
}
}).prepare(t).engine
cnr := cidtest.ID()
generateAndPutObject := func() *objectSDK.Object {
obj := testutil.GenerateObjectWithCID(cnr)
testutil.AddAttribute(obj, objectV2.SysAttributeExpEpoch, strconv.Itoa(objectExpiresAfter))
var putPrm PutPrm
putPrm.Object = obj
require.NoError(t, engine.Put(context.Background(), putPrm))
return obj
}
t.Run("inhume with tombstone", func(t *testing.T) {
obj := generateAndPutObject()
ts := oidtest.Address()
ts.SetContainer(cnr)
var prm InhumePrm
prm.WithTarget(ts, object.AddressOf(obj))
err := engine.Inhume(context.Background(), prm)
require.NoError(t, err)
})
t.Run("inhume without tombstone", func(t *testing.T) {
obj := generateAndPutObject()
var prm InhumePrm
prm.MarkAsGarbage(object.AddressOf(obj))
err := engine.Inhume(context.Background(), prm)
require.NoError(t, err)
})
}
func BenchmarkInhumeMultipart(b *testing.B) {
// The benchmark result insignificantly depends on the number of shards,
// so do not use it as a benchmark parameter, just set it big enough.
numShards := 100
for numObjects := 1; numObjects <= 10000; numObjects *= 10 {
b.Run(
fmt.Sprintf("objects=%d", numObjects),
func(b *testing.B) {
benchmarkInhumeMultipart(b, numShards, numObjects)
},
)
}
}
func benchmarkInhumeMultipart(b *testing.B, numShards, numObjects int) {
b.StopTimer()
engine := testNewEngine(b).
setShardsNum(b, numShards).prepare(b).engine
defer func() { require.NoError(b, engine.Close(context.Background())) }()
cnt := cidtest.ID()
eg := errgroup.Group{}
for range b.N {
addrs := make([]oid.Address, numObjects)
for i := range numObjects {
prm := PutPrm{}
prm.Object = objecttest.Object().Parent()
prm.Object.SetContainerID(cnt)
prm.Object.SetType(objectSDK.TypeRegular)
addrs[i] = object.AddressOf(prm.Object)
eg.Go(func() error {
return engine.Put(context.Background(), prm)
})
}
require.NoError(b, eg.Wait())
ts := oidtest.Address()
ts.SetContainer(cnt)
prm := InhumePrm{}
prm.WithTarget(ts, addrs...)
b.StartTimer()
err := engine.Inhume(context.Background(), prm)
require.NoError(b, err)
b.StopTimer()
}
}
func TestInhumeIfObjectDoesntExist(t *testing.T) {
const numShards = 4
engine := testNewEngine(t).setShardsNum(t, numShards).prepare(t).engine
t.Cleanup(func() { require.NoError(t, engine.Close(context.Background())) })
t.Run("inhume without tombstone", func(t *testing.T) {
testInhumeIfObjectDoesntExist(t, engine, false, false)
})
t.Run("inhume with tombstone", func(t *testing.T) {
testInhumeIfObjectDoesntExist(t, engine, true, false)
})
t.Run("force inhume", func(t *testing.T) {
testInhumeIfObjectDoesntExist(t, engine, false, true)
})
t.Run("object is locked", func(t *testing.T) {
t.Run("inhume without tombstone", func(t *testing.T) {
testInhumeLockedIfObjectDoesntExist(t, engine, false, false)
})
t.Run("inhume with tombstone", func(t *testing.T) {
testInhumeLockedIfObjectDoesntExist(t, engine, true, false)
})
t.Run("force inhume", func(t *testing.T) {
testInhumeLockedIfObjectDoesntExist(t, engine, false, true)
})
})
}
func testInhumeIfObjectDoesntExist(t *testing.T, e *StorageEngine, withTombstone, withForce bool) {
t.Parallel()
object := oidtest.Address()
require.NoError(t, testInhumeObject(t, e, object, withTombstone, withForce))
err := testHeadObject(e, object)
if withTombstone {
require.True(t, client.IsErrObjectAlreadyRemoved(err))
} else {
require.True(t, client.IsErrObjectNotFound(err))
}
}
func testInhumeLockedIfObjectDoesntExist(t *testing.T, e *StorageEngine, withTombstone, withForce bool) {
t.Parallel()
object := oidtest.Address()
require.NoError(t, testLockObject(e, object))
err := testInhumeObject(t, e, object, withTombstone, withForce)
if !withForce {
var errLocked *apistatus.ObjectLocked
require.ErrorAs(t, err, &errLocked)
return
}
require.NoError(t, err)
err = testHeadObject(e, object)
if withTombstone {
require.True(t, client.IsErrObjectAlreadyRemoved(err))
} else {
require.True(t, client.IsErrObjectNotFound(err))
}
}
func testLockObject(e *StorageEngine, obj oid.Address) error {
return e.Lock(context.Background(), obj.Container(), oidtest.ID(), []oid.ID{obj.Object()})
}
func testInhumeObject(t testing.TB, e *StorageEngine, obj oid.Address, withTombstone, withForce bool) error {
tombstone := oidtest.Address()
tombstone.SetContainer(obj.Container())
// Due to the tests design it is possible to set both the options,
// however removal with tombstone and force removal are exclusive.
require.False(t, withTombstone && withForce)
var inhumePrm InhumePrm
if withTombstone {
inhumePrm.WithTarget(tombstone, obj)
} else {
inhumePrm.MarkAsGarbage(obj)
}
if withForce {
inhumePrm.WithForceRemoval()
}
return e.Inhume(context.Background(), inhumePrm)
}
func testHeadObject(e *StorageEngine, obj oid.Address) error {
var headPrm HeadPrm
headPrm.WithAddress(obj)
_, err := e.Head(context.Background(), headPrm)
return err
}