s3api: skip TTL fast-path for versioned buckets (#8823)

* s3api: skip TTL fast-path for versioned buckets (#8757)

PutBucketLifecycleConfiguration was translating Expiration.Days into
filer.conf TTL entries for all buckets. For versioned buckets this is
wrong:

 1. TTL volumes expire as a unit, destroying all data — including
    noncurrent versions that should be preserved.
 2. Filer-backend TTL (RocksDB compaction filter, Redis key expiry)
    removes entries without triggering chunk deletion, leaving orphaned
    volume data with 0 deleted bytes.
 3. On AWS S3, Expiration.Days on a versioned bucket creates a delete
    marker — it does not hard-delete data. TTL has no such nuance.

Fix: skip the TTL fast-path when the bucket has versioning enabled or
suspended. All lifecycle rules are evaluated at scan time by the
lifecycle worker instead.

Also fix the lifecycle worker to evaluate Expiration rules against the
latest version in .versions/ directories, which was previously skipped
entirely — only NoncurrentVersionExpiration was handled.

* lifecycle worker: handle SeaweedList error in versions dir cleanup

Do not assume the directory is empty when the list call fails — log
the error and skip the directory to avoid incorrect deletion.

* address review feedback

- Fetch version file for tag-based rules instead of reading tags from
  the .versions directory entry where they are not cached.
- Handle getBucketVersioningStatus error by failing closed (treat as
  versioned) to avoid creating TTL entries on transient failures.
- Capture and assert deleteExpiredObjects return values in test.
- Improve test documentation.
This commit is contained in:
Chris Lu
2026-03-29 00:05:53 -07:00
committed by GitHub
parent 9dd43ca006
commit e8a6fcaafb
4 changed files with 461 additions and 1 deletions

View File

@@ -10,6 +10,7 @@ import (
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -124,3 +125,39 @@ func (f failingReadCloser) Read(_ []byte) (int, error) {
func (f failingReadCloser) Close() error {
return nil
}
// TestShouldSkipTTLFastPathForVersionedBuckets verifies the versioning guard
// logic that PutBucketLifecycleConfigurationHandler uses to decide whether
// to create filer.conf TTL entries. On AWS S3, Expiration.Days on a versioned
// bucket creates a delete marker — it does not delete data. TTL volumes
// would destroy all versions indiscriminately, so the lifecycle worker must
// handle versioned buckets at scan time instead. (issue #8757)
//
// Note: an integration test that invokes PutBucketLifecycleConfigurationHandler
// directly is not feasible here because the handler requires filer gRPC
// connectivity (ReadFilerConfFromFilers, WithFilerClient) before it reaches
// the versioning check. The lifecycle worker integration tests in
// weed/plugin/worker/lifecycle/ cover the end-to-end versioned-bucket behavior.
func TestShouldSkipTTLFastPathForVersionedBuckets(t *testing.T) {
tests := []struct {
name string
versioning string
expectSkip bool
}{
{"versioning_enabled_skips_ttl", s3_constants.VersioningEnabled, true},
{"versioning_suspended_skips_ttl", s3_constants.VersioningSuspended, true},
{"unversioned_allows_ttl", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// This mirrors the guard in PutBucketLifecycleConfigurationHandler:
// isVersioned := versioningErr != s3err.ErrNone ||
// bucketVersioning == s3_constants.VersioningEnabled ||
// bucketVersioning == s3_constants.VersioningSuspended
// When isVersioned is true, the TTL fast-path is skipped entirely.
isVersioned := tt.versioning == s3_constants.VersioningEnabled ||
tt.versioning == s3_constants.VersioningSuspended
assert.Equal(t, tt.expectSkip, isVersioned)
})
}
}