mirror of
https://github.com/pgsty/minio.git
synced 2026-11-10 17:45:32 +03:00
test(ilm): cover the history majority, approval check and tier ownership gates
Three safeguards of the lifecycle and tier cleanup paths had no test that failed when they were removed: - authoritativeHistory accepts a revision only on more than half of the drives. TestPoolsLifecycleHalfDriveMarker holds a delete marker on exactly half of sixteen drives, which is the read quorum of a marker, and checks that the history is refused, that the expiry of the version under it and of the key are refused and every drive keeps what it has, and that one more drive decides. With the guard removed the strict merge at the same majority still refuses the history; with the merge relaxed as well the test fails, so it pins the refusal, not one of its two layers. - deleteAllApproved refuses a purge whose placement read differs from the versions it approved. deleteAllBeforePlacement is a test-only injection point between the two reads, nil in production. TestPurgeAllVersionsRefusesChangedApproval writes a new version and rewrites the latest one there, as a writer that bypasses the object lock would, and checks the write quorum refusal, the history_unavailable count, that nothing is deleted and that the next purge removes the key. With the check disabled the purge deletes the versions it never approved. - readTierHistory refuses the ownership of a remote object while a drive of any pool cannot be read, and xlMetaV2.DeleteVersion deletes a free record only with the remote tuple it names. TestTierOwnershipReadFailsClosed fails one drive of the pool that holds nothing of the key as offline, not found, faulty and unformatted, and checks that the expiry worker's and the decommission's cleanup are refused with a read quorum error and succeed once the drive reads again. TestFreeRecordDeleteNeedsMatchingTuple deletes a free record with another tier, remote object and remote version, in memory and on an xlStorage drive, and checks the record stays until the tuple matches. With either check removed the remote object or the record is deleted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -723,6 +723,73 @@ func TestPurgeAllVersionsRefusesStaleDecision(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The purge reads the placement of the versions it approved and may only
|
||||
// confirm them. A write that reaches the key between the approval and that
|
||||
// read, as one that bypasses the object lock would, changes what the purge
|
||||
// is about to delete: a version it did not approve, or another write of an
|
||||
// approved one. The purge refuses the key with a write quorum error, which
|
||||
// counts as an unavailable history, and deletes nothing; the next purge,
|
||||
// which approves what it finds, removes the key.
|
||||
func TestPurgeAllVersionsRefusesChangedApproval(t *testing.T) {
|
||||
for _, kind := range []string{"new-version", "latest-rewritten"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
z := purgeSinglePool(t, 16)
|
||||
p := newPurgeTest(t, z, "purge-approval", MakeBucketOptions{VersioningEnabled: true}, purgeRules)
|
||||
ctx := t.Context()
|
||||
const key = "ea/approval"
|
||||
p.version(0, key, "v1", 5)
|
||||
latest := p.version(0, key, "v2", 4)
|
||||
ops := p.scanQueue(0, key)
|
||||
|
||||
// Each purge finds the history changed since its approval.
|
||||
var writes int
|
||||
deleteAllBeforePlacement = func(bucket, object string) {
|
||||
if bucket != p.bucket || object != key {
|
||||
return
|
||||
}
|
||||
writes++
|
||||
label := fmt.Sprintf("v%d", 2+writes)
|
||||
opts := ObjectOptions{Versioned: true, NoLock: true}
|
||||
mtime := daysAgo(3)
|
||||
if kind == "latest-rewritten" {
|
||||
label = fmt.Sprintf("v2-rewrite-%d", writes)
|
||||
opts.VersionID, mtime = latest.VersionID, latest.ModTime
|
||||
}
|
||||
p.put(0, key, label, mtime, opts)
|
||||
}
|
||||
t.Cleanup(func() { deleteAllBeforePlacement = nil })
|
||||
|
||||
_, err := z.DeleteObject(ctx, p.bucket, key, ObjectOptions{DeleteAllVersions: true, Expiration: ExpirationOptions{Expire: true}})
|
||||
if !isErrWriteQuorum(err) {
|
||||
t.Fatalf("purge of a changed history: %v, want a write quorum error", err)
|
||||
}
|
||||
delta := incompleteDelta(func() { p.run(ops) })
|
||||
if delta[ilmIncompleteHistoryUnavailable] != 1 {
|
||||
t.Errorf("counted %v, want one %s", delta, ilmIncompleteHistoryUnavailable)
|
||||
}
|
||||
if writes != 2 {
|
||||
t.Fatalf("the history changed %d times, want 2", writes)
|
||||
}
|
||||
want, read := []string{"hot", "hot", "hot", "hot"}, "v4"
|
||||
if kind == "latest-rewritten" {
|
||||
want, read = []string{"hot", "hot"}, "v2-rewrite-2"
|
||||
}
|
||||
if got := p.held(0, key); !slices.Equal(got, want) {
|
||||
t.Fatalf("the refused purges left %v, want %v", got, want)
|
||||
}
|
||||
if got := p.get(key); got != read {
|
||||
t.Errorf("the key reads %s, want %s", got, read)
|
||||
}
|
||||
|
||||
deleteAllBeforePlacement = nil
|
||||
p.expire(0, key)
|
||||
if got := p.held(0, key); got != nil {
|
||||
t.Errorf("the purge of the unchanged history left %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Control: on two pools the stale decision was already refused.
|
||||
func TestPurgeAllVersionsStaleDecisionTwoPoolsControl(t *testing.T) {
|
||||
z, _ := consistencyPools(t)
|
||||
|
||||
Reference in New Issue
Block a user