test(pools): make the copy and bulk lock tests race-free and stricter

The implementation review of the lock fixes found test flaws, which are
corrected here; the production behavior is unchanged.

- The alias and merge tests replaced globalOperationTimeout while
  background healing reads it, a data race under -race. They now bound
  each request with its own context instead.
- The alias test now holds the cleaned name's write lock while the copy
  runs, so it proves the names are checked before anything is locked;
  it checks both objects before any later write, and covers a/./x as a
  destination.
- The merge test picks aliases that hash to the clean name's set and,
  where sets have their own lockers, ones that do not, and requires the
  alias's own table to hold a read lock: readers admitted, writers
  excluded.
- Release polling creates a fresh lock instance for every probe.
- The getNSLocks event hook and the bulk delete group limit are atomic,
  so tests can set them while other goroutines read them.

Without the name check and the resource merge, both tests now fail in
every lock layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-11-06 19:25:56 +08:00
parent b32a389e9d
commit 1780eb8ffc
4 changed files with 94 additions and 44 deletions
+17 -7
View File
@@ -24,6 +24,7 @@ import (
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/minio/minio-go/v7/pkg/set"
@@ -153,11 +154,11 @@ const (
// getNSLocksEvent, when a test sets it, is told when getNSLocks gives up a
// bounded wait ("bounded-timeout"), starts to wait for that lock alone
// ("idle-wait") and got it ("idle-acquired"), with the index of the lock.
var getNSLocksEvent func(event string, index int)
var getNSLocksEvent atomic.Pointer[func(event string, index int)]
func nsLocksEvent(event string, index int) {
if f := getNSLocksEvent; f != nil {
f(event, index)
if f := getNSLocksEvent.Load(); f != nil {
(*f)(event, index)
}
}
@@ -344,7 +345,7 @@ func (z *erasureServerPools) deleteObjectsInSets(ctx context.Context, bucket str
}
slices.Sort(order)
sem := make(chan struct{}, min(len(order), deleteSetGroupsLimit))
sem := make(chan struct{}, min(len(order), deleteSetGroupsLimitValue()))
var wg sync.WaitGroup
for _, idx := range order {
members := groups[idx]
@@ -359,9 +360,18 @@ func (z *erasureServerPools) deleteObjectsInSets(ctx context.Context, bucket str
wg.Wait()
}
// deleteSetGroupsLimit bounds how many sets a bulk delete of one pool works
// on at once.
var deleteSetGroupsLimit = 16
// deleteSetGroupsDefault bounds how many sets a bulk delete of one pool works
// on at once; a test can lower it through deleteSetGroupsLimit.
const deleteSetGroupsDefault = 16
var deleteSetGroupsLimit atomic.Int32
func deleteSetGroupsLimitValue() int {
if n := int(deleteSetGroupsLimit.Load()); n > 0 {
return n
}
return deleteSetGroupsDefault
}
// deleteObjectsInSet deletes the members of objects, which all hash to er,
// under er's lock, and writes their results. Nothing else writes them.