mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
* test: cover delete idempotency on versioned object-locked buckets Backup clients probe and retract lock keys continuously, so they routinely delete keys and versions that are already gone, and they batch those deletes alongside keys that do exist. S3 makes all of that succeed; returning an error turns ordinary lock arbitration into a job failure. The behaviour is correct today but had no coverage, and it runs through the object-lock retention check, which is the most likely place for a missing object to start being reported as an error. Covers: deleting a key that never existed, deleting a well-formed version id that names nothing (twice, and without disturbing the version that does exist), and a batch whose middle key is missing — every requested key must come back under its own name rather than silently taking another row's slot. * test: verify the deletes actually took effect, not just that they returned
157 lines
5.6 KiB
Go
157 lines
5.6 KiB
Go
package s3api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/aws"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Backup clients probe and retract lock keys constantly, so they delete keys and
|
|
// versions that may already be gone. S3 makes those deletes succeed; answering an
|
|
// error instead turns routine lock arbitration into a job failure. Object lock is
|
|
// enabled here because the retention check runs before the delete and is the most
|
|
// likely place for a missing object to be reported as an error.
|
|
|
|
func TestDeleteMissingObjectSucceeds(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
// A key that never existed.
|
|
resp, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String("never-written.lock"),
|
|
})
|
|
require.NoError(t, err, "deleting a key that never existed must succeed")
|
|
require.NotNil(t, resp.DeleteMarker, "a versioned bucket records the delete as a marker")
|
|
assert.True(t, *resp.DeleteMarker)
|
|
require.NotNil(t, resp.VersionId)
|
|
|
|
// The response can claim a marker the backend never stored, so confirm it is
|
|
// really in the version history under the id the response handed back.
|
|
versions, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String("never-written.lock"),
|
|
})
|
|
require.NoError(t, err)
|
|
found := false
|
|
for _, m := range versions.DeleteMarkers {
|
|
if m.Key != nil && *m.Key == "never-written.lock" && m.VersionId != nil && *m.VersionId == *resp.VersionId {
|
|
found = true
|
|
}
|
|
}
|
|
assert.True(t, found, "the delete marker reported by the response must be persisted")
|
|
}
|
|
|
|
func TestDeleteAbsentVersionSucceeds(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
objectKey := "arbitration.lock"
|
|
put, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
Body: bytes.NewReader([]byte("held")),
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, put.VersionId)
|
|
|
|
// Well-formed version id that names no stored version. Deleting it is a no-op,
|
|
// not an error, and must not disturb the version that does exist.
|
|
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: aws.String("deadbeefdeadbeefdeadbeefdeadbeef"),
|
|
})
|
|
require.NoError(t, err, "deleting a version that does not exist must succeed")
|
|
|
|
head, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: put.VersionId,
|
|
})
|
|
require.NoError(t, err, "the real version must survive the no-op delete")
|
|
require.NotNil(t, head.VersionId)
|
|
assert.Equal(t, *put.VersionId, *head.VersionId)
|
|
|
|
// Repeating the delete of the real version is also a no-op rather than an error.
|
|
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: put.VersionId,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Confirm the first delete actually removed the version. Without this a
|
|
// backend that ignored every version-specific delete would satisfy both calls
|
|
// and the idempotency the test claims to cover would never be exercised.
|
|
_, err = client.HeadObject(context.TODO(), &s3.HeadObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: put.VersionId,
|
|
})
|
|
require.Error(t, err, "the version must be gone after the first delete")
|
|
|
|
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: put.VersionId,
|
|
})
|
|
require.NoError(t, err, "repeating a version delete must stay a no-op")
|
|
}
|
|
|
|
// A missing key in the middle of a batch must not consume another key's result
|
|
// slot: every request key has to come back under its own name.
|
|
func TestMultiObjectDeleteAttributesMissingKeys(t *testing.T) {
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucketWithObjectLock(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
present := []string{"batch-1.lock", "batch-3.lock"}
|
|
for _, key := range present {
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
Body: bytes.NewReader([]byte("held")),
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
requested := []string{"batch-1.lock", "batch-2-missing.lock", "batch-3.lock"}
|
|
objects := make([]types.ObjectIdentifier, 0, len(requested))
|
|
for _, key := range requested {
|
|
objects = append(objects, types.ObjectIdentifier{Key: aws.String(key)})
|
|
}
|
|
|
|
resp, err := client.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Delete: &types.Delete{Objects: objects},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp.Errors, "a missing key is not a batch error")
|
|
require.Len(t, resp.Deleted, len(requested))
|
|
|
|
got := make(map[string]bool, len(resp.Deleted))
|
|
for _, d := range resp.Deleted {
|
|
require.NotNil(t, d.Key, "every result row must carry the key it belongs to")
|
|
assert.NotEmpty(t, *d.Key, "an empty key means a row lost its identity")
|
|
got[*d.Key] = true
|
|
}
|
|
for _, key := range requested {
|
|
assert.True(t, got[key], "no result row for %s", key)
|
|
}
|
|
}
|