test(listing): cover mixed object and directory recovery groups

Extend the pending-directory collision test with absent recovery and a smaller
key. Add all six normal-object, directory, and pending-object channel orders,
checking complete-group recovery and preservation of trailing entries.

Test changes adapted from commit 557bc274a32330321fa5e5ac7636068c7bd98eb0.
Retain the complete-group directory handling from 3dd53eb81f.

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-10-18 07:23:05 +08:00
parent 9954392605
commit 2080b3d031
2 changed files with 104 additions and 25 deletions
+62
View File
@@ -179,3 +179,65 @@ func TestListingRecoveryDirectoryCollisionOrdering(t *testing.T) {
} }
} }
} }
// A regular walk result may be stale when a pending peer forces current
// recovery. Keep sibling prefixes until the complete object group is resolved.
func TestListingRecoveryMixedDirectoryCollision(t *testing.T) {
orders := [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
for _, order := range orders {
for _, present := range []bool{false, true} {
t.Run(fmt.Sprintf("order=%v/present=%t", order, present), func(t *testing.T) {
entries := []metaCacheEntry{
{name: "dir/", metadata: []byte{1}},
{name: "dir/"},
{name: "dir/", listingSet: &erasureObjects{}},
}
in := make([]chan metaCacheEntry, len(order))
for i, index := range order {
in[i] = make(chan metaCacheEntry, 2)
in[i] <- entries[index]
in[i] <- metaCacheEntry{name: fmt.Sprintf("tail-%d", i), metadata: []byte{1}}
close(in[i])
}
out := make(chan metaCacheEntry, 6)
calls := 0
err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) {
calls++
if len(group) != 2 {
t.Fatalf("incomplete object group: %+v", group)
}
pending := 0
for _, entry := range group {
if entry.name != "dir/" || entry.isDir() {
t.Fatalf("unexpected recovery entry: %+v", entry)
}
if entry.listingSet != nil {
pending++
}
}
if pending != 1 {
t.Fatalf("pending entries=%d, want 1", pending)
}
if !present {
return nil, nil
}
return &metaCacheEntry{name: "dir/", metadata: []byte{1}}, nil
})
if err != nil || calls != 1 {
t.Fatalf("err=%v recovery calls=%d", err, calls)
}
var names []string
for entry := range out {
names = append(names, entry.name)
if entry.listingSet != nil || entry.name == "dir/" && entry.isObject() != present {
t.Fatalf("unexpected merged entry: %+v", entry)
}
}
want := []string{"dir/", "tail-0", "tail-1", "tail-2"}
if !slices.Equal(names, want) {
t.Fatalf("names=%v, want %v", names, want)
}
})
}
}
}
+22 -5
View File
@@ -221,34 +221,51 @@ func TestListingRecoveryNormalPathDoesNotReadOrLock(t *testing.T) {
func TestListingPendingDirectoryCollision(t *testing.T) { func TestListingPendingDirectoryCollision(t *testing.T) {
for _, objectFirst := range []bool{false, true} { for _, objectFirst := range []bool{false, true} {
t.Run(fmt.Sprint(objectFirst), func(t *testing.T) { for _, present := range []bool{true, false} {
in := []chan metaCacheEntry{make(chan metaCacheEntry, 1), make(chan metaCacheEntry, 1)} t.Run(fmt.Sprintf("objectFirst=%v/present=%v", objectFirst, present), func(t *testing.T) {
in := []chan metaCacheEntry{make(chan metaCacheEntry, 2), make(chan metaCacheEntry, 2), make(chan metaCacheEntry, 2)}
objectIndex := 1 objectIndex := 1
if objectFirst { if objectFirst {
objectIndex = 0 objectIndex = 0
} }
in[objectIndex] <- metaCacheEntry{name: "dir/", listingSet: &erasureObjects{}} in[objectIndex] <- metaCacheEntry{name: "dir/", listingSet: &erasureObjects{}}
in[1-objectIndex] <- metaCacheEntry{name: "dir/"} in[1-objectIndex] <- metaCacheEntry{name: "dir/"}
in[2] <- metaCacheEntry{name: "a", metadata: []byte{1}}
for _, c := range in { for _, c := range in {
close(c) close(c)
} }
out := make(chan metaCacheEntry, 2) out := make(chan metaCacheEntry, 4)
calls := 0 calls := 0
err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) { err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) {
calls++ calls++
if len(group) != 1 || group[0].listingSet == nil { if len(group) != 1 || group[0].listingSet == nil {
t.Fatalf("wrong pending group: %+v", group) t.Fatalf("wrong pending group: %+v", group)
} }
if !present {
return nil, nil
}
return &metaCacheEntry{name: "dir/", metadata: []byte{1}}, nil return &metaCacheEntry{name: "dir/", metadata: []byte{1}}, nil
}) })
if err != nil || calls != 1 || len(out) != 1 { if err != nil || calls != 1 || len(out) != 2 {
t.Fatalf("err=%v recoveries=%d outputs=%d", err, calls, len(out)) t.Fatalf("err=%v recoveries=%d outputs=%d", err, calls, len(out))
} }
if entry := <-out; entry.isDir() || !entry.isObjectDir() { first := <-out
if first.name != "a" {
t.Fatalf("expected smaller key first, got %+v", first)
}
entry := <-out
if entry.name != "dir/" || entry.listingSet != nil {
t.Fatalf("unexpected recovered entry: %+v", entry)
}
if present && (entry.isDir() || !entry.isObjectDir()) {
t.Fatalf("object became directory: %+v", entry) t.Fatalf("object became directory: %+v", entry)
} }
if !present && (!entry.isDir() || entry.name != "dir/") {
t.Fatalf("absent pending object dropped live directory prefix: %+v", entry)
}
}) })
} }
}
} }
func TestListingRecoveryRejectsUnexpectedSinglePoolSet(t *testing.T) { func TestListingRecoveryRejectsUnexpectedSinglePoolSet(t *testing.T) {