mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 09:35:31 +03:00
fix(ilm): do not announce batch expiries the recheck rejected
The expiry worker now asks DeleteObjects to recheck ILM against all pools. A rejected entry keeps its version and returns errLifecycleNotApplicable, but the worker still wrote an ILMExpiry audit record, an ILM trace and an s3:ObjectRemoved:Delete event for it. Event delivery drops the error element, so consumers saw an ordinary removal, repeated on every scan while the versions stay split across pools. Skip rejected entries; eligible entries of the same batch are still reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -23,9 +23,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio/internal/bucket/lifecycle"
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
||||||
"github.com/minio/minio/internal/bucket/replication"
|
"github.com/minio/minio/internal/bucket/replication"
|
||||||
"github.com/minio/minio/internal/bucket/versioning"
|
"github.com/minio/minio/internal/bucket/versioning"
|
||||||
|
"github.com/minio/minio/internal/event"
|
||||||
|
"github.com/minio/minio/internal/pubsub"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reuse the author's scanner/worker fixture. A second configured pool is
|
// Reuse the author's scanner/worker fixture. A second configured pool is
|
||||||
@@ -212,3 +215,56 @@ func TestPoolsLifecycleTieredBatchIdentity(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A batch entry whose expiry the all-pools recheck rejects is kept. The worker
|
||||||
|
// must not report it as removed, by event, ILM trace or audit, while eligible
|
||||||
|
// entries of the same batch are still reported.
|
||||||
|
func TestPoolsLifecycleBatchRejectionNotAnnounced(t *testing.T) {
|
||||||
|
s := newSplitILM(t, versioning.Enabled, splitILMRules)
|
||||||
|
const key = "nnv-announce"
|
||||||
|
for _, v := range []splitVersion{splitTiered("v1", 5, 0), splitTiered("v2", 4, 0), splitTiered("v3", 3, 0), splitAt("v4", 2, 0)} {
|
||||||
|
s.write(key, v)
|
||||||
|
}
|
||||||
|
eligible, retained := s.ids[key+" v1"], s.ids[key+" v3"]
|
||||||
|
done := make(chan struct{})
|
||||||
|
defer close(done)
|
||||||
|
events := make(chan event.Event, 8)
|
||||||
|
if err := globalHTTPListen.Subscribe(pubsub.MaskFromMaskable(event.ObjectRemovedDelete), events, done, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
traces := make(chan madmin.TraceInfo, 8)
|
||||||
|
if err := globalTrace.Subscribe(madmin.TraceILM, traces, done, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result := &lifecycleBatchResult{ObjectLayer: s.z}
|
||||||
|
deleteObjectVersions(t.Context(), result, s.bucket, []ObjectToDelete{
|
||||||
|
{ObjectV: ObjectV{ObjectName: key, VersionID: eligible}},
|
||||||
|
{ObjectV: ObjectV{ObjectName: key, VersionID: retained}},
|
||||||
|
}, []lifecycle.Event{{Action: lifecycle.DeleteVersionAction}, {Action: lifecycle.DeleteVersionAction}})
|
||||||
|
if result.errs[0] != nil || !errors.Is(result.errs[1], errLifecycleNotApplicable) {
|
||||||
|
t.Fatalf("fixture results: eligible=%v retained=%v", result.errs[0], result.errs[1])
|
||||||
|
}
|
||||||
|
if _, err := s.z.GetObjectInfo(t.Context(), s.bucket, key, ObjectOptions{VersionID: retained, Versioned: true}); err != nil {
|
||||||
|
t.Fatalf("rejected version must remain: %v", err)
|
||||||
|
}
|
||||||
|
// Both publishers deliver synchronously, so everything sent is buffered.
|
||||||
|
var notified, traced []string
|
||||||
|
for drained := false; !drained; {
|
||||||
|
select {
|
||||||
|
case ev := <-events:
|
||||||
|
notified = append(notified, ev.S3.Object.VersionID)
|
||||||
|
case tr := <-traces:
|
||||||
|
if tr.FuncName == ILMExpiry {
|
||||||
|
traced = append(traced, tr.Custom["version-id"])
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
drained = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(notified) != 1 || notified[0] != eligible {
|
||||||
|
t.Errorf("removal events for %v, want only eligible %s (retained %s)", notified, eligible, retained)
|
||||||
|
}
|
||||||
|
if len(traced) != 1 || traced[0] != eligible {
|
||||||
|
t.Errorf("ILM expiry traces for %v, want only eligible %s (retained %s)", traced, eligible, retained)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -471,6 +472,11 @@ func deleteObjectVersions(ctx context.Context, o ObjectLayer, bucket string, toD
|
|||||||
})
|
})
|
||||||
|
|
||||||
for i, dobj := range deletedObjs {
|
for i, dobj := range deletedObjs {
|
||||||
|
if errors.Is(errs[i], errLifecycleNotApplicable) {
|
||||||
|
// The recheck kept this version. Do not audit or announce a
|
||||||
|
// removal that did not happen; the next scan re-evaluates it.
|
||||||
|
continue
|
||||||
|
}
|
||||||
oi := ObjectInfo{
|
oi := ObjectInfo{
|
||||||
Bucket: bucket,
|
Bucket: bucket,
|
||||||
Name: dobj.ObjectName,
|
Name: dobj.ObjectName,
|
||||||
|
|||||||
Reference in New Issue
Block a user