From 794e59ea61388862d00fdee0318f7b6f63edb8c1 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 9 May 2026 23:20:24 -0700 Subject: [PATCH] test(s3/lifecycle): integration coverage for config update between sweeps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An operator changes the lifecycle rule between two shell-driven sweeps. The second sweep must respect the NEW rule, not a cached copy of the old one. Each runLifecycleShard invocation spawns a fresh weed shell subprocess, so cached engine state from a previous sweep doesn't persist — but a regression that caches rules across PutBucketLifecycleConfiguration calls within the S3 server itself would still surface here. Sweep 1: rule prefix="first/", PUT + backdate firstKey, run worker → firstKey expires. Update rule to prefix="second/", PUT + backdate secondKey AND a new key under the OLD prefix ("first/post-update.txt"). Sweep 2 must expire only the second-prefix object; the post-update old- prefix one must survive — config replacement, not merge. --- .../s3_lifecycle_config_update_test.go | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/s3/lifecycle/s3_lifecycle_config_update_test.go diff --git a/test/s3/lifecycle/s3_lifecycle_config_update_test.go b/test/s3/lifecycle/s3_lifecycle_config_update_test.go new file mode 100644 index 000000000..887ff5c9c --- /dev/null +++ b/test/s3/lifecycle/s3_lifecycle_config_update_test.go @@ -0,0 +1,79 @@ +// Lifecycle config update across sweeps. +package lifecycle + +import ( + "context" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/require" +) + +// TestLifecycleConfigUpdateBetweenSweeps: an operator changes the +// lifecycle rule between two shell-driven sweeps. The second sweep +// must respect the NEW rule, not a cached version of the old one. +// +// Each `runLifecycleShard` invocation spawns a fresh `weed shell` +// subprocess, so cached engine state from a previous sweep doesn't +// persist across runs. This test pins that the freshly-loaded config +// actually changes routing — under the new prefix only matching +// objects expire, even if there are still backdated objects sitting +// under the old prefix. +func TestLifecycleConfigUpdateBetweenSweeps(t *testing.T) { + c := s3Client(t) + fc, fcClose := filerClient(t) + defer fcClose() + + bucket := uniqueBucket("config-update") + mustCreateBucket(t, c, bucket) + + // Sweep 1: rule expires anything under "first/". + putExpirationLifecycle(t, c, bucket, "first/", 1) + + const firstKey = "first/initial.txt" + putObject(t, c, bucket, firstKey, "first") + backdateMtime(t, fc, bucket, firstKey, 30) + + out := runLifecycleShard(t) + t.Logf("sweep 1 output:\n%s", out) + + require.Eventuallyf(t, func() bool { + _, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{ + Bucket: aws.String(bucket), Key: aws.String(firstKey), + }) + return isS3NotFound(err) + }, 30*time.Second, 500*time.Millisecond, "sweep 1 must expire %s", firstKey) + + // Update the rule to a different prefix. The old "first/" prefix + // is no longer covered by any rule; objects under it must NOT be + // expired by sweep 2 even when backdated. + putExpirationLifecycle(t, c, bucket, "second/", 1) + + const secondKey = "second/new.txt" + const oldPrefixKey = "first/post-update.txt" + putObject(t, c, bucket, secondKey, "second") + putObject(t, c, bucket, oldPrefixKey, "stale rule") + backdateMtime(t, fc, bucket, secondKey, 30) + backdateMtime(t, fc, bucket, oldPrefixKey, 30) + + out = runLifecycleShard(t) + t.Logf("sweep 2 output:\n%s", out) + + // Sweep 2 expires the new-prefix object. + require.Eventuallyf(t, func() bool { + _, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{ + Bucket: aws.String(bucket), Key: aws.String(secondKey), + }) + return isS3NotFound(err) + }, 30*time.Second, 500*time.Millisecond, "sweep 2 must expire %s under the new rule", secondKey) + + // Sweep 2 must NOT expire the old-prefix object — the rule was + // replaced, not merged. A regression that caches old rules across + // PutBucketLifecycleConfiguration calls would fail here. + _, err := c.HeadObject(context.Background(), &s3.HeadObjectInput{ + Bucket: aws.String(bucket), Key: aws.String(oldPrefixKey), + }) + require.NoError(t, err, "old-prefix object must survive after rule update — config replacement, not merge") +}