Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
474 lines
16 KiB
Go
474 lines
16 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// TestVersioningPaginationOver1000Versions tests that ListObjectVersions correctly
|
|
// handles objects with more than 1000 versions using pagination.
|
|
// This is a stress test that creates 1500 versions and verifies all are listed.
|
|
//
|
|
// Run with: ENABLE_STRESS_TESTS=true go test -v -run TestVersioningPaginationOver1000Versions -timeout 30m
|
|
func TestVersioningPaginationOver1000Versions(t *testing.T) {
|
|
if os.Getenv("ENABLE_STRESS_TESTS") != "true" {
|
|
t.Skip("Skipping stress test. Set ENABLE_STRESS_TESTS=true to run.")
|
|
}
|
|
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
// Create bucket
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
// Enable versioning
|
|
enableVersioning(t, client, bucketName)
|
|
checkVersioningStatus(t, client, bucketName, types.BucketVersioningStatusEnabled)
|
|
|
|
objectKey := "test-many-versions"
|
|
// Use 1100 for CI (tests pagination boundary), can override with env var for quick local testing
|
|
numVersions := 1100
|
|
if quickTest := os.Getenv("QUICK_TEST"); quickTest == "true" {
|
|
numVersions = 50
|
|
}
|
|
versionIds := make([]string, 0, numVersions)
|
|
|
|
t.Logf("Creating %d versions of object %s...", numVersions, objectKey)
|
|
startTime := time.Now()
|
|
|
|
// Create many versions of the same object
|
|
for i := 0; i < numVersions; i++ {
|
|
content := fmt.Sprintf("content-version-%d", i+1)
|
|
putResp, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
Body: strings.NewReader(content),
|
|
})
|
|
require.NoError(t, err, "Failed to create version %d", i+1)
|
|
require.NotNil(t, putResp.VersionId)
|
|
versionIds = append(versionIds, *putResp.VersionId)
|
|
|
|
if (i+1)%100 == 0 {
|
|
t.Logf("Created %d/%d versions...", i+1, numVersions)
|
|
}
|
|
}
|
|
|
|
createDuration := time.Since(startTime)
|
|
t.Logf("Created %d versions in %v", numVersions, createDuration)
|
|
t.Logf("Version IDs collected: %d", len(versionIds))
|
|
|
|
// Quick sanity check - list versions to see what we have
|
|
quickResp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(objectKey),
|
|
})
|
|
require.NoError(t, err)
|
|
t.Logf("Quick check: got %d versions, %d delete markers, truncated=%v",
|
|
len(quickResp.Versions), len(quickResp.DeleteMarkers),
|
|
quickResp.IsTruncated != nil && *quickResp.IsTruncated)
|
|
|
|
// Test 1: List all versions without pagination limit
|
|
t.Run("ListAllVersionsNoPagination", func(t *testing.T) {
|
|
allVersions := listAllVersions(t, client, bucketName, objectKey)
|
|
t.Logf("Got %d versions total", len(allVersions))
|
|
assert.Len(t, allVersions, numVersions, "Should have all %d versions", numVersions)
|
|
|
|
// Verify all version IDs are unique
|
|
versionIdSet := make(map[string]bool)
|
|
for _, v := range allVersions {
|
|
assert.False(t, versionIdSet[*v.VersionId], "Duplicate version ID: %s", *v.VersionId)
|
|
versionIdSet[*v.VersionId] = true
|
|
}
|
|
|
|
// Verify only one version is marked as latest
|
|
latestCount := 0
|
|
for _, v := range allVersions {
|
|
if *v.IsLatest {
|
|
latestCount++
|
|
assert.Equal(t, versionIds[numVersions-1], *v.VersionId, "Latest should be the last created")
|
|
}
|
|
}
|
|
assert.Equal(t, 1, latestCount, "Only one version should be marked as latest")
|
|
})
|
|
|
|
// Test 2: List with explicit small max-keys to force multiple pages
|
|
t.Run("ListWithSmallMaxKeys", func(t *testing.T) {
|
|
maxKeys := int32(100)
|
|
allVersions := make([]types.ObjectVersion, 0, numVersions)
|
|
var keyMarker, versionIdMarker *string
|
|
pageCount := 0
|
|
|
|
for {
|
|
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(objectKey),
|
|
MaxKeys: aws.Int32(maxKeys),
|
|
KeyMarker: keyMarker,
|
|
VersionIdMarker: versionIdMarker,
|
|
})
|
|
require.NoError(t, err)
|
|
pageCount++
|
|
|
|
allVersions = append(allVersions, resp.Versions...)
|
|
|
|
if resp.IsTruncated == nil || !*resp.IsTruncated {
|
|
break
|
|
}
|
|
keyMarker = resp.NextKeyMarker
|
|
versionIdMarker = resp.NextVersionIdMarker
|
|
}
|
|
|
|
t.Logf("Listed %d versions in %d pages (max-keys=%d)", len(allVersions), pageCount, maxKeys)
|
|
assert.Len(t, allVersions, numVersions, "Should have all %d versions", numVersions)
|
|
if numVersions > int(maxKeys) {
|
|
assert.Greater(t, pageCount, 1, "Should require multiple pages when versions > max-keys")
|
|
}
|
|
})
|
|
|
|
// Test 3: List with max-keys=1000 (S3 default) to test the boundary
|
|
t.Run("ListWithDefaultMaxKeys", func(t *testing.T) {
|
|
maxKeys := int32(1000)
|
|
allVersions := make([]types.ObjectVersion, 0, numVersions)
|
|
var keyMarker, versionIdMarker *string
|
|
pageCount := 0
|
|
|
|
for {
|
|
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(objectKey),
|
|
MaxKeys: aws.Int32(maxKeys),
|
|
KeyMarker: keyMarker,
|
|
VersionIdMarker: versionIdMarker,
|
|
})
|
|
require.NoError(t, err)
|
|
pageCount++
|
|
|
|
allVersions = append(allVersions, resp.Versions...)
|
|
t.Logf("Page %d: got %d versions, truncated=%v", pageCount, len(resp.Versions), resp.IsTruncated != nil && *resp.IsTruncated)
|
|
|
|
if resp.IsTruncated == nil || !*resp.IsTruncated {
|
|
break
|
|
}
|
|
keyMarker = resp.NextKeyMarker
|
|
versionIdMarker = resp.NextVersionIdMarker
|
|
}
|
|
|
|
t.Logf("Listed %d versions in %d pages (max-keys=%d)", len(allVersions), pageCount, maxKeys)
|
|
assert.Len(t, allVersions, numVersions, "Should have all %d versions", numVersions)
|
|
if numVersions > 1000 {
|
|
assert.GreaterOrEqual(t, pageCount, 2, "Should require at least 2 pages for >1000 versions with max-keys=1000")
|
|
}
|
|
})
|
|
|
|
// Test 4: Verify we can retrieve specific versions
|
|
t.Run("RetrieveSpecificVersions", func(t *testing.T) {
|
|
// Test first, middle, and last versions
|
|
testIndices := []int{0, numVersions / 2, numVersions - 1}
|
|
for _, idx := range testIndices {
|
|
expectedContent := fmt.Sprintf("content-version-%d", idx+1)
|
|
getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
VersionId: aws.String(versionIds[idx]),
|
|
})
|
|
require.NoError(t, err, "Failed to get version at index %d", idx)
|
|
|
|
body, err := io.ReadAll(getResp.Body)
|
|
getResp.Body.Close()
|
|
require.NoError(t, err, "Failed to read version at index %d", idx)
|
|
|
|
assert.Equal(t, expectedContent, string(body), "Content mismatch for version %d", idx+1)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestVersioningPaginationMultipleObjectsManyVersions tests pagination with multiple objects
|
|
// each having many versions, ensuring correct handling of key-marker and version-id-marker.
|
|
//
|
|
// Run with: ENABLE_STRESS_TESTS=true go test -v -run TestVersioningPaginationMultipleObjectsManyVersions -timeout 30m
|
|
func TestVersioningPaginationMultipleObjectsManyVersions(t *testing.T) {
|
|
if os.Getenv("ENABLE_STRESS_TESTS") != "true" {
|
|
t.Skip("Skipping stress test. Set ENABLE_STRESS_TESTS=true to run.")
|
|
}
|
|
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
enableVersioning(t, client, bucketName)
|
|
checkVersioningStatus(t, client, bucketName, types.BucketVersioningStatusEnabled)
|
|
|
|
numObjects := 5
|
|
versionsPerObject := 300
|
|
totalExpectedVersions := numObjects * versionsPerObject
|
|
|
|
t.Logf("Creating %d objects with %d versions each (%d total)...", numObjects, versionsPerObject, totalExpectedVersions)
|
|
startTime := time.Now()
|
|
|
|
// Create multiple objects with many versions each
|
|
for i := 0; i < numObjects; i++ {
|
|
objectKey := fmt.Sprintf("object-%03d", i)
|
|
for j := 0; j < versionsPerObject; j++ {
|
|
content := fmt.Sprintf("obj%d-ver%d", i, j+1)
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(objectKey),
|
|
Body: strings.NewReader(content),
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
t.Logf("Created object %s with %d versions", objectKey, versionsPerObject)
|
|
}
|
|
|
|
createDuration := time.Since(startTime)
|
|
t.Logf("Created %d total versions in %v", totalExpectedVersions, createDuration)
|
|
|
|
// List all versions using pagination
|
|
t.Run("ListAllWithPagination", func(t *testing.T) {
|
|
maxKeys := int32(500)
|
|
allVersions := make([]types.ObjectVersion, 0, totalExpectedVersions)
|
|
var keyMarker, versionIdMarker *string
|
|
pageCount := 0
|
|
|
|
for {
|
|
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
MaxKeys: aws.Int32(maxKeys),
|
|
KeyMarker: keyMarker,
|
|
VersionIdMarker: versionIdMarker,
|
|
})
|
|
require.NoError(t, err)
|
|
pageCount++
|
|
|
|
allVersions = append(allVersions, resp.Versions...)
|
|
t.Logf("Page %d: got %d versions, truncated=%v", pageCount, len(resp.Versions), resp.IsTruncated != nil && *resp.IsTruncated)
|
|
|
|
if resp.IsTruncated == nil || !*resp.IsTruncated {
|
|
break
|
|
}
|
|
keyMarker = resp.NextKeyMarker
|
|
versionIdMarker = resp.NextVersionIdMarker
|
|
}
|
|
|
|
t.Logf("Listed %d versions in %d pages", len(allVersions), pageCount)
|
|
assert.Len(t, allVersions, totalExpectedVersions, "Should have all versions")
|
|
|
|
// Verify each object has correct number of versions
|
|
versionsByKey := make(map[string]int)
|
|
for _, v := range allVersions {
|
|
versionsByKey[*v.Key]++
|
|
}
|
|
assert.Len(t, versionsByKey, numObjects, "Should have %d unique objects", numObjects)
|
|
for key, count := range versionsByKey {
|
|
assert.Equal(t, versionsPerObject, count, "Object %s should have %d versions", key, versionsPerObject)
|
|
}
|
|
|
|
// Verify each object has exactly one latest version
|
|
latestByKey := make(map[string]int)
|
|
for _, v := range allVersions {
|
|
if *v.IsLatest {
|
|
latestByKey[*v.Key]++
|
|
}
|
|
}
|
|
for key, count := range latestByKey {
|
|
assert.Equal(t, 1, count, "Object %s should have exactly one latest version", key)
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestVersioningPaginationDeepDirectoryHierarchy tests that paginated ListObjectVersions
|
|
// correctly skips directory subtrees before the key-marker. This reproduces the
|
|
// real-world scenario where Veeam backup objects are spread across many subdirectories
|
|
// (e.g., Mailboxes/<uuid>/ItemsData/<file>) and pagination becomes exponentially
|
|
// slower as the marker advances through the tree.
|
|
//
|
|
// Run with: ENABLE_STRESS_TESTS=true go test -v -run TestVersioningPaginationDeepDirectoryHierarchy -timeout 10m
|
|
func TestVersioningPaginationDeepDirectoryHierarchy(t *testing.T) {
|
|
if os.Getenv("ENABLE_STRESS_TESTS") != "true" {
|
|
t.Skip("Skipping stress test. Set ENABLE_STRESS_TESTS=true to run.")
|
|
}
|
|
|
|
client := getS3Client(t)
|
|
bucketName := getNewBucketName()
|
|
|
|
createBucket(t, client, bucketName)
|
|
defer deleteBucket(t, client, bucketName)
|
|
|
|
enableVersioning(t, client, bucketName)
|
|
checkVersioningStatus(t, client, bucketName, types.BucketVersioningStatusEnabled)
|
|
|
|
// Create a deep directory structure mimicking Veeam 365 backup layout:
|
|
// Backup/Organizations/<org>/Mailboxes/<mailbox>/ItemsData/<file>
|
|
numMailboxes := 20
|
|
filesPerMailbox := 5
|
|
totalObjects := numMailboxes * filesPerMailbox
|
|
orgPrefix := "Backup/Organizations/org-001/Mailboxes"
|
|
|
|
t.Logf("Creating %d objects across %d subdirectories (depth=6)...", totalObjects, numMailboxes)
|
|
startTime := time.Now()
|
|
|
|
allKeys := make([]string, 0, totalObjects)
|
|
for i := 0; i < numMailboxes; i++ {
|
|
mailboxId := fmt.Sprintf("mbx-%03d", i)
|
|
for j := 0; j < filesPerMailbox; j++ {
|
|
key := fmt.Sprintf("%s/%s/ItemsData/file-%03d.dat", orgPrefix, mailboxId, j)
|
|
_, err := client.PutObject(context.TODO(), &s3.PutObjectInput{
|
|
Bucket: aws.String(bucketName),
|
|
Key: aws.String(key),
|
|
Body: strings.NewReader(fmt.Sprintf("content-%d-%d", i, j)),
|
|
})
|
|
require.NoError(t, err)
|
|
allKeys = append(allKeys, key)
|
|
}
|
|
}
|
|
t.Logf("Created %d objects in %v", totalObjects, time.Since(startTime))
|
|
|
|
// Test 1: Paginate through all versions with a broad prefix and small maxKeys.
|
|
// This forces multiple pages that must skip earlier subdirectory trees.
|
|
t.Run("PaginateAcrossSubdirectories", func(t *testing.T) {
|
|
maxKeys := int32(10) // Force many pages to exercise marker skipping
|
|
var allVersions []types.ObjectVersion
|
|
var keyMarker, versionIdMarker *string
|
|
pageCount := 0
|
|
pageStartTimes := make([]time.Duration, 0)
|
|
|
|
for {
|
|
pageStart := time.Now()
|
|
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(orgPrefix + "/"),
|
|
MaxKeys: aws.Int32(maxKeys),
|
|
KeyMarker: keyMarker,
|
|
VersionIdMarker: versionIdMarker,
|
|
})
|
|
pageDuration := time.Since(pageStart)
|
|
require.NoError(t, err)
|
|
pageCount++
|
|
pageStartTimes = append(pageStartTimes, pageDuration)
|
|
|
|
allVersions = append(allVersions, resp.Versions...)
|
|
t.Logf("Page %d: %d versions in %v (marker: %v)",
|
|
pageCount, len(resp.Versions), pageDuration,
|
|
keyMarker)
|
|
|
|
if resp.IsTruncated == nil || !*resp.IsTruncated {
|
|
break
|
|
}
|
|
keyMarker = resp.NextKeyMarker
|
|
versionIdMarker = resp.NextVersionIdMarker
|
|
}
|
|
|
|
assert.Greater(t, pageCount, 1, "Should require multiple pages")
|
|
|
|
// Verify listed keys exactly match the keys we created (same elements, same order)
|
|
listedKeys := make([]string, 0, len(allVersions))
|
|
for _, v := range allVersions {
|
|
listedKeys = append(listedKeys, *v.Key)
|
|
}
|
|
assert.Equal(t, allKeys, listedKeys,
|
|
"Listed version keys should exactly match created keys")
|
|
|
|
// Check that later pages don't take dramatically longer than earlier ones.
|
|
// Before the fix, later pages were exponentially slower because they
|
|
// re-traversed the entire tree. With the fix, all pages should be similar.
|
|
if len(pageStartTimes) >= 4 {
|
|
firstQuarter := pageStartTimes[0]
|
|
lastQuarter := pageStartTimes[len(pageStartTimes)-1]
|
|
t.Logf("First page: %v, Last page: %v, Ratio: %.1fx",
|
|
firstQuarter, lastQuarter,
|
|
float64(lastQuarter)/float64(firstQuarter))
|
|
// Allow generous 10x ratio to avoid flakiness; before the fix
|
|
// the ratio was 100x+ on large datasets
|
|
if lastQuarter > firstQuarter*10 && lastQuarter > 500*time.Millisecond {
|
|
t.Errorf("Last page took %.1fx longer than first page (%v vs %v) — possible pagination regression",
|
|
float64(lastQuarter)/float64(firstQuarter), lastQuarter, firstQuarter)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Test 2: Paginate with delimiter to verify CommonPrefixes interaction
|
|
t.Run("PaginateWithDelimiterAcrossSubdirs", func(t *testing.T) {
|
|
maxKeys := int32(5)
|
|
var allPrefixes []string
|
|
var keyMarker *string
|
|
pageCount := 0
|
|
|
|
for {
|
|
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(orgPrefix + "/"),
|
|
Delimiter: aws.String("/"),
|
|
MaxKeys: aws.Int32(maxKeys),
|
|
KeyMarker: keyMarker,
|
|
})
|
|
require.NoError(t, err)
|
|
pageCount++
|
|
|
|
for _, cp := range resp.CommonPrefixes {
|
|
allPrefixes = append(allPrefixes, *cp.Prefix)
|
|
}
|
|
|
|
if resp.IsTruncated == nil || !*resp.IsTruncated {
|
|
break
|
|
}
|
|
keyMarker = resp.NextKeyMarker
|
|
}
|
|
|
|
assert.Greater(t, pageCount, 1, "Should require multiple pages with maxKeys=%d", maxKeys)
|
|
|
|
// Build the exact expected prefixes
|
|
expectedPrefixes := make([]string, 0, numMailboxes)
|
|
for i := 0; i < numMailboxes; i++ {
|
|
expectedPrefixes = append(expectedPrefixes,
|
|
fmt.Sprintf("%s/mbx-%03d/", orgPrefix, i))
|
|
}
|
|
assert.Equal(t, expectedPrefixes, allPrefixes,
|
|
"CommonPrefixes should exactly match expected mailbox prefixes")
|
|
})
|
|
}
|
|
|
|
// listAllVersions is a helper to list all versions of a specific object using pagination
|
|
func listAllVersions(t *testing.T, client *s3.Client, bucketName, objectKey string) []types.ObjectVersion {
|
|
var allVersions []types.ObjectVersion
|
|
var keyMarker, versionIdMarker *string
|
|
pageCount := 0
|
|
|
|
for {
|
|
resp, err := client.ListObjectVersions(context.TODO(), &s3.ListObjectVersionsInput{
|
|
Bucket: aws.String(bucketName),
|
|
Prefix: aws.String(objectKey),
|
|
KeyMarker: keyMarker,
|
|
VersionIdMarker: versionIdMarker,
|
|
})
|
|
require.NoError(t, err)
|
|
pageCount++
|
|
|
|
t.Logf("Page %d: got %d versions, truncated=%v", pageCount, len(resp.Versions), resp.IsTruncated != nil && *resp.IsTruncated)
|
|
allVersions = append(allVersions, resp.Versions...)
|
|
|
|
// Check if truncated (handle nil pointer)
|
|
if resp.IsTruncated == nil || !*resp.IsTruncated {
|
|
break
|
|
}
|
|
keyMarker = resp.NextKeyMarker
|
|
versionIdMarker = resp.NextVersionIdMarker
|
|
}
|
|
|
|
t.Logf("Total: %d versions in %d pages", len(allVersions), pageCount)
|
|
return allVersions
|
|
}
|