// Copyright (c) 2026 Feng Ruohang // // This file is part of Silo Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package cmd import ( "fmt" "math/rand" "reflect" "slices" "testing" "time" ) // Use complete metadata so the selected version can also be decoded by LIST // and the object read path, rather than testing only synthetic shallow headers. func quorumNullVersion(t testing.TB, generation int64) xlMetaV2ShallowVersion { t.Helper() fi := newFileInfo("fixed/object", 12, 4) fi.Erasure.Index = 1 fi.DataDir = "11111111-1111-1111-1111-111111111111" fi.ModTime = time.Unix(generation, 0).UTC() fi.Size = 8192 fi.Parts = []ObjectPartInfo{{Number: 1, Size: fi.Size, ActualSize: fi.Size}} fi.Metadata = map[string]string{"etag": fmt.Sprintf("%032d", generation)} var xl xlMetaV2 if err := xl.AddVersion(fi); err != nil { t.Fatal(err) } return xl.versions[0] } // Start with forward, reverse and interleaved orders, then fixed-seed shuffles. func quorumVersionOrders(input [][]xlMetaV2ShallowVersion) [][][]xlMetaV2ShallowVersion { orders := [][][]xlMetaV2ShallowVersion{slices.Clone(input), slices.Clone(input)} slices.Reverse(orders[1]) interleaved := make([][]xlMetaV2ShallowVersion, 0, len(input)) for i, j := 0, len(input)-1; i <= j; i, j = i+1, j-1 { interleaved = append(interleaved, input[i]) if i != j { interleaved = append(interleaved, input[j]) } } orders = append(orders, interleaved) for seed := range int64(32) { order := slices.Clone(input) rand.New(rand.NewSource(seed)).Shuffle(len(order), func(i, j int) { order[i], order[j] = order[j], order[i] }) orders = append(orders, order) } return orders } func TestMergeXLV2SingleNullQuorum(t *testing.T) { v := []xlMetaV2ShallowVersion{quorumNullVersion(t, 100), quorumNullVersion(t, 200), quorumNullVersion(t, 300)} for _, tt := range []struct { name string counts [3]int empty int quorum int want int }{ {"consistent", [3]int{16}, 0, 8, 0}, {"old9-new7", [3]int{9, 7}, 0, 8, 0}, {"old15-new1", [3]int{15, 1}, 0, 8, 0}, {"old3-new1", [3]int{3, 1}, 0, 3, 0}, {"two-quorums", [3]int{8, 8}, 0, 8, 1}, {"empty-streams", [3]int{9, 1}, 6, 8, 0}, {"two-subquorums", [3]int{7, 7}, 2, 8, -1}, {"three-subquorums", [3]int{6, 5, 5}, 0, 8, -1}, } { t.Run(tt.name, func(t *testing.T) { var input [][]xlMetaV2ShallowVersion for g, n := range tt.counts { for range n { input = append(input, []xlMetaV2ShallowVersion{v[g]}) } } input = append(input, make([][]xlMetaV2ShallowVersion, tt.empty)...) want := []xlMetaV2ShallowVersion{} if tt.want >= 0 { want = append(want, v[tt.want]) } for order, versions := range quorumVersionOrders(input) { for _, strict := range []bool{false, true} { for _, requested := range []int{0, 1} { got := mergeXLV2Versions(tt.quorum, strict, requested, versions...) if !reflect.DeepEqual(got, want) { t.Fatalf("order=%d strict=%v requested=%d: got %#v, want %#v", order, strict, requested, got, want) } } } } }) } } func TestMergeXLV2SingleNullHeaderGroups(t *testing.T) { old := quorumNullVersion(t, 100) newer := quorumNullVersion(t, 200) differentSig := old differentSig.header.Signature[0]++ differentFlags := old differentFlags.header.Flags ^= xlFlagInlineData for _, tt := range []struct { name string input [][]xlMetaV2ShallowVersion strict bool want []xlMetaV2ShallowVersion }{ {"signature-nonstrict", [][]xlMetaV2ShallowVersion{{old}, {old}, {differentSig}, {newer}}, false, []xlMetaV2ShallowVersion{differentSig}}, {"signature-strict", [][]xlMetaV2ShallowVersion{{old}, {old}, {differentSig}, {newer}}, true, []xlMetaV2ShallowVersion{}}, {"flags-nonstrict", [][]xlMetaV2ShallowVersion{{old}, {old}, {differentFlags}, {newer}}, false, []xlMetaV2ShallowVersion{}}, {"flags-strict", [][]xlMetaV2ShallowVersion{{old}, {old}, {differentFlags}, {newer}}, true, []xlMetaV2ShallowVersion{}}, } { t.Run(tt.name, func(t *testing.T) { got := mergeXLV2Versions(3, tt.strict, 0, tt.input...) if !reflect.DeepEqual(got, tt.want) { t.Fatalf("got %#v, want %#v", got, tt.want) } }) } } func TestResolveSingleNullQuorum(t *testing.T) { old, newer := quorumNullVersion(t, 100), quorumNullVersion(t, 200) input := make([][]xlMetaV2ShallowVersion, 16) for i := range input { input[i] = []xlMetaV2ShallowVersion{old} if i >= 9 { input[i] = []xlMetaV2ShallowVersion{newer} } } for order, versions := range quorumVersionOrders(input) { entries := make(metaCacheEntries, len(versions)) for i := range versions { xl := &xlMetaV2{versions: versions[i]} metadata, err := xl.AppendTo(nil) if err != nil { t.Fatal(err) } entries[i] = metaCacheEntry{name: "fixed/object", metadata: metadata} } selected, ok := entries.resolve(&metadataResolutionParams{objQuorum: 8, dirQuorum: 8, requestedVersions: 1}) if !ok { t.Fatalf("order %d: a complete null object version has quorum but was omitted", order) } xl, err := selected.xlmeta() if err != nil { t.Fatal(err) } if !reflect.DeepEqual(xl.versions, []xlMetaV2ShallowVersion{old}) { t.Fatalf("order %d: incorrect complete selected version: %#v", order, xl.versions) } fi, err := xl.ToFileInfo("bucket", "fixed/object", "", false, false) if err != nil || !fi.IsValid() || fi.Size != 8192 || !fi.ModTime.Equal(time.Unix(100, 0)) { t.Fatalf("order %d: selected metadata cannot be read: %+v, %v", order, fi, err) } } } // These are compatibility assertions for inputs outside the narrow repair. // In particular, the mixed-version outputs below are not a new correctness // contract for version enumeration; that behavior requires a separate change. func TestMergeXLV2NullHistoriesUnchanged(t *testing.T) { old, newer := quorumNullVersion(t, 100), quorumNullVersion(t, 300) middle := quorumNullVersion(t, 200) middle.header.VersionID = [16]byte{1} highest := quorumNullVersion(t, 400) highest.header.VersionID = [16]byte{2} for _, tt := range []struct { name string input [][]xlMetaV2ShallowVersion want []xlMetaV2ShallowVersion }{ {"mixed-forward", [][]xlMetaV2ShallowVersion{{old}, {old}, {old}, {newer, middle}, {middle}, {middle}}, []xlMetaV2ShallowVersion{middle}}, {"mixed-reverse", [][]xlMetaV2ShallowVersion{{middle}, {middle}, {newer, middle}, {old}, {old}, {old}}, []xlMetaV2ShallowVersion{old}}, {"history-pruned-first", [][]xlMetaV2ShallowVersion{{highest, old}, {highest, old}, {highest, old}, {highest, newer}}, []xlMetaV2ShallowVersion{highest}}, } { t.Run(tt.name, func(t *testing.T) { got := mergeXLV2Versions(3, false, 0, tt.input...) if !reflect.DeepEqual(got, tt.want) { t.Fatalf("excluded history changed: got %#v, want %#v", got, tt.want) } }) } for _, kind := range []string{"delete", "free", "legacy", "mixed-ec", "legacy-modern-ec", "nonzero-strict"} { t.Run(kind, func(t *testing.T) { a, b := old, newer switch kind { case "delete": b.header.Type = DeleteType case "free": b.header.Flags |= xlFlagFreeVersion case "legacy": b.header.Type = LegacyType case "mixed-ec": b.header.EcN, b.header.EcM = 8, 8 case "legacy-modern-ec": b.header.EcN, b.header.EcM = 0, 0 case "nonzero-strict": a.header.VersionID, b.header.VersionID = [16]byte{1}, [16]byte{1} } for _, requested := range []int{0, 1} { got := mergeXLV2Versions(3, true, requested, []xlMetaV2ShallowVersion{a}, []xlMetaV2ShallowVersion{a}, []xlMetaV2ShallowVersion{a}, []xlMetaV2ShallowVersion{b}) if len(got) != 0 { t.Fatalf("excluded input changed: requested=%d got %#v", requested, got) } } }) } // All-legacy EC headers are eligible, unlike mixing legacy and modern EC. old.header.EcN, old.header.EcM = 0, 0 newer.header.EcN, newer.header.EcM = 0, 0 got := mergeXLV2Versions(3, false, 0, []xlMetaV2ShallowVersion{old}, []xlMetaV2ShallowVersion{old}, []xlMetaV2ShallowVersion{old}, []xlMetaV2ShallowVersion{newer}) if !reflect.DeepEqual(got, []xlMetaV2ShallowVersion{old}) { t.Fatalf("all-legacy EC lost the old quorum: %#v", got) } } func BenchmarkResolveSingleNullQuorumPage(b *testing.B) { old, newer := quorumNullVersion(b, 100), quorumNullVersion(b, 200) for _, kind := range []string{"consistent", "old-first", "new-first"} { b.Run(kind, func(b *testing.B) { var templates [16]metaCacheEntry for i := range templates { v := old if kind == "old-first" && i >= 9 || kind == "new-first" && i < 7 { v = newer } xl := &xlMetaV2{versions: []xlMetaV2ShallowVersion{v}} data, err := xl.AppendTo(nil) if err != nil { b.Fatal(err) } templates[i] = metaCacheEntry{metadata: data} } var names [1000]string for i := range names { names[i] = fmt.Sprintf("fixed/%04d", i) } resolver := metadataResolutionParams{objQuorum: 8, dirQuorum: 8, requestedVersions: 1} b.ReportAllocs() b.ResetTimer() for b.Loop() { for _, name := range names { entries := templates for i := range entries { entries[i].name = name } selected, ok := metaCacheEntries(entries[:]).resolve(&resolver) if ok && selected.reusable { metaDataPoolPut(selected.metadata) } } } }) } }