mirror of
https://github.com/pgsty/minio.git
synced 2026-11-12 10:35:33 +03:00
b5d97e3e1f
Keep restored versions consistent across expiry and successive heals while preserving metadata, layout and non-object revision distinctions. Fixes #344 Signed-off-by: Feng Ruohang <rh@vonng.com>
142 lines
5.2 KiB
Go
142 lines
5.2 KiB
Go
// Copyright (c) 2026 Feng Ruohang
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/minio/madmin-go/v3"
|
|
"github.com/minio/minio/internal/bucket/lifecycle"
|
|
"github.com/minio/minio/internal/bucket/versioning"
|
|
xhttp "github.com/minio/minio/internal/http"
|
|
)
|
|
|
|
func TestVersionRevisionRestoreFlags(t *testing.T) {
|
|
base := authorityRevision(t, [16]byte{1}, 1, 1, "")
|
|
cleared := base
|
|
cleared.header.Flags &^= xlFlagUsesDataDir
|
|
set := base
|
|
set.header.Flags |= xlFlagUsesDataDir
|
|
if versionRevision(cleared, xlMetaVersion) != versionRevision(set, xlMetaVersion) {
|
|
t.Fatal("derived flag splits one revision")
|
|
}
|
|
for _, change := range []struct {
|
|
name string
|
|
apply func(*xlMetaV2Object)
|
|
}{
|
|
{"data-dir", func(o *xlMetaV2Object) { o.DataDir[0]++ }},
|
|
{"restore", func(o *xlMetaV2Object) { o.MetaUser[xhttp.AmzRestore] = `ongoing-request="true"` }},
|
|
{"retention", func(o *xlMetaV2Object) { o.MetaUser["x-amz-object-lock-mode"] = "COMPLIANCE" }},
|
|
{"layout", func(o *xlMetaV2Object) { o.ErasureM, o.ErasureN = 8, 8 }},
|
|
} {
|
|
t.Run(change.name, func(t *testing.T) {
|
|
var v xlMetaV2Version
|
|
if _, err := v.unmarshalV(xlMetaVersion, base.meta); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
change.apply(v.ObjectV2)
|
|
meta, err := v.MarshalMsg(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
different := xlMetaV2ShallowVersion{header: cleared.header, meta: meta}
|
|
if versionRevision(different, xlMetaVersion) == versionRevision(set, xlMetaVersion) {
|
|
t.Fatal("real metadata change lost")
|
|
}
|
|
})
|
|
}
|
|
// Every other header flag remains part of identity, even if metadata is equal.
|
|
for flag := xlFlags(1); flag != 0; flag <<= 1 {
|
|
if flag == xlFlagUsesDataDir {
|
|
continue
|
|
}
|
|
other := base
|
|
other.header.Flags ^= flag
|
|
if versionRevision(other, xlMetaVersion) == versionRevision(base, xlMetaVersion) {
|
|
t.Fatalf("flag %d was normalized", flag)
|
|
}
|
|
}
|
|
malformed := base
|
|
malformed.meta = []byte{0xc1}
|
|
other := malformed
|
|
other.header.Flags ^= xlFlagUsesDataDir
|
|
if versionRevision(malformed, xlMetaVersion) == versionRevision(other, xlMetaVersion) {
|
|
t.Fatal("undecodable metadata was normalized")
|
|
}
|
|
marker := authorityMarker(t, [16]byte{2}, 2)
|
|
other = marker
|
|
other.header.Flags ^= xlFlagUsesDataDir
|
|
if versionRevision(marker, xlMetaVersion) == versionRevision(other, xlMetaVersion) {
|
|
t.Fatal("delete marker was normalized as an object")
|
|
}
|
|
}
|
|
|
|
// Two drives are replaced one at a time after the restore expires. The old
|
|
// headers describe the pre-expiry state; heal writes the current derived flag.
|
|
func TestRestoreExpiryAcrossSuccessiveHeals(t *testing.T) {
|
|
r := newRestoreSafetyTest(t, 2, versioning.Enabled)
|
|
ctx := t.Context()
|
|
oi := r.put("restored", false, true)
|
|
r.restore(oi)
|
|
before := r.fileInfo(oi.VersionID)
|
|
disks := r.z.serverPools[0].getHashedSet(r.object).getDisks()
|
|
// Model the headers written before wall-clock expiry without a timing race.
|
|
for _, disk := range disks {
|
|
rewriteTierTestMeta(t, disk, r.bucket, r.object, func(xl *xlMetaV2) { xl.versions[0].header.Flags |= xlFlagUsesDataDir })
|
|
}
|
|
for _, i := range []int{3, 2} {
|
|
if err := disks[i].Delete(ctx, r.bucket, r.object, DeleteOptions{Recursive: true, Immediate: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := r.z.HealObject(ctx, r.bucket, r.object, oi.VersionID, madmin.HealOpts{ScanMode: madmin.HealNormalScan}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for i, disk := range disks {
|
|
raw, err := disk.ReadXL(ctx, r.bucket, r.object, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var xl xlMetaV2
|
|
if err = xl.Load(raw.Buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := xl.versions[0].header.Flags&xlFlagUsesDataDir != 0; got != (i < 2) {
|
|
t.Fatalf("drive %d has unexpected derived flag %v", i, got)
|
|
}
|
|
}
|
|
if _, _, err := r.z.lifecycleEventsAllPools(ctx, r.bucket, r.object); err != nil {
|
|
t.Fatalf("history with split flags: %v", err)
|
|
}
|
|
current, err := r.z.GetObjectInfo(ctx, r.bucket, r.object, r.opts(oi.VersionID))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = expireTransitionedObject(ctx, r.z, ¤t, lifecycle.Event{Action: lifecycle.DeleteRestoredAction}, lcEventSrc_Scanner); err != nil {
|
|
t.Fatalf("expire restored copy: %v", err)
|
|
}
|
|
if n := r.dataDirs(before.DataDir); n != 0 {
|
|
t.Fatalf("restore cleanup left %d data directories", n)
|
|
}
|
|
if got := readCopyChecksumObject(t, r.z, r.bucket, r.object, r.opts(oi.VersionID)); string(got) != string(r.bodies["restored"]) {
|
|
t.Fatal("tiered object is no longer readable")
|
|
}
|
|
r.put("newer", false, false)
|
|
if _, _, err = r.z.lifecycleEventsAllPools(ctx, r.bucket, r.object); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = r.z.deleteAllHistory(ctx, r.bucket, r.object); err != nil {
|
|
t.Fatalf("purge history with split flags: %v", err)
|
|
}
|
|
// The rule permits tiering, not a purge. Normalizing the header must not
|
|
// turn a readable history into permission to remove the versions.
|
|
if _, err = r.z.DeleteObject(ctx, r.bucket, r.object, ObjectOptions{DeleteAllVersions: true, Expiration: ExpirationOptions{Expire: true}}); !errors.Is(err, errLifecycleNotApplicable) {
|
|
t.Fatalf("purge without an applicable rule: %v", err)
|
|
}
|
|
if _, err = r.z.GetObjectInfo(ctx, r.bucket, r.object, r.opts(oi.VersionID)); err != nil {
|
|
t.Fatalf("refused purge removed the tiered version: %v", err)
|
|
}
|
|
}
|