e2etest/stress_generators/gen_many_folders.go (149 lines of code) (raw):
package main
import (
"context"
"fmt"
"github.com/Azure/azure-storage-azcopy/v10/common"
"github.com/Azure/azure-storage-azcopy/v10/e2etest"
"github.com/google/uuid"
"github.com/spf13/pflag"
"golang.org/x/sync/semaphore"
"math/rand/v2"
"strings"
"sync"
"sync/atomic"
)
const (
GenManyFoldersName = "many-folders"
)
func init() {
RegisterGenerator(&ManyFoldersGenerator{})
}
type ManyFoldersGenerator struct {
ContainerTarget string
preferredService string
}
func (m *ManyFoldersGenerator) PreferredService() common.Location {
if m.preferredService != "" {
var out common.Location
_ = out.Parse(m.preferredService)
return out
}
return common.ELocation.File()
}
func (m *ManyFoldersGenerator) Name() string {
return GenManyFoldersName
}
func (m *ManyFoldersGenerator) Generate(manager e2etest.ServiceResourceManager) error {
const (
FolderSize = 5
MinDepth = 5
MaxDepth = 10
FolderCount = 1_000_000
// mostly unused but preserved for the IDE hint
TotalObjectCount = (FolderSize * FolderCount) + FolderCount
)
var depthMap = e2etest.NewRWMutexResource(make(map[int]int64))
a := &DummyAsserter{}
container := manager.GetContainer(m.ContainerTarget)
if container.Exists() {
return fmt.Errorf("please delete the container %s before re-running the generator", m.ContainerTarget)
}
container.Create(a, e2etest.ContainerProperties{})
if a.CaughtError != nil {
return fmt.Errorf("failed to create parent container %w", a.CaughtError)
}
gjm := NewGenerationJobManager(TotalObjectCount, CommonGenerateAnnouncementIncrement)
gjm.CustomAnnounce = func() string {
var out string
depthMap.DoRead(func(res map[int]int64) {
out = fmt.Sprint(res)
})
return "folder depths: " + out
}
// We limit the number of things we're trying to queue up at once so we don't casually use 64gb of memory for funsies
allocationCap := semaphore.NewWeighted(100_000)
genDirectoryName := func() string {
var out string
out = uuid.NewString()
out = out[:strings.IndexRune(out, '-')]
return out
}
cPath := make([]string, 0)
trieMu := &sync.Mutex{}
trie := e2etest.NewTrie[bool]('/')
// should have a lock before calling genDirectoryNameSafe
genDirectoryNameSafe := func() string {
root := strings.Join(cPath, "/")
var out string
for {
out = genDirectoryName()
if trie.Get(root+"/"+out) == nil {
break
}
}
return out
}
trie.Insert(strings.Join(cPath, "/"), e2etest.PtrOf(true))
for range FolderCount {
for len(cPath) < MinDepth {
cPath = append(cPath, genDirectoryNameSafe())
trie.Insert(strings.Join(cPath, "/"), e2etest.PtrOf(true))
}
// generate the path out here
folderPath := strings.Join(cPath, "/")
// schedule creation
gjm.ScheduleItem(func() error {
a := &DummyAsserter{}
depthMap.DoWrite(func(res map[int]int64) {
res[len(strings.Split(folderPath, "/"))]++
})
if folderPath != "" {
folder := container.GetObject(a, folderPath, common.EEntityType.Folder())
folder.Create(a, e2etest.NewZeroObjectContentContainer(0), e2etest.ObjectProperties{})
if a.CaughtError != nil {
return fmt.Errorf("failed to create parent folder: %w", a.CaughtError)
}
}
return nil
}, false)
for range FolderSize {
_ = allocationCap.Acquire(context.Background(), 1)
trieMu.Lock()
filePath := folderPath + "/" + genDirectoryName()
for e2etest.DerefOrZero(trie.Get(filePath)) {
filePath = folderPath + "/" + genDirectoryName()
}
trie.Insert(filePath, e2etest.PtrOf(true))
trieMu.Unlock()
gjm.ScheduleItem(func() error {
a := &DummyAsserter{}
defer allocationCap.Release(1)
file := container.GetObject(a, filePath, common.EEntityType.File())
file.Create(a, e2etest.NewRandomObjectContentContainer(50), e2etest.ObjectProperties{})
if a.CaughtError != nil {
return fmt.Errorf("failed to create child object: %w", a.CaughtError)
}
return nil
}, true) // create files with prio
}
trieMu.Lock()
// path traversal, ensure we always get a new directory, and ensure there's some complexity to our tree.
// random chance to ascend up the tree; or if we are at root we *must* ascend.
if rNum := rand.IntN(101); (len(cPath) < MaxDepth && rNum > 60) || len(cPath) <= MinDepth {
cPath = append(cPath, genDirectoryNameSafe())
} else if rNum > 25 { // if we're 25-60, stay level, but switch to a new dir.
cPath = cPath[:len(cPath)-1]
} else {
if len(cPath) == 1 {
// descend to root if we're at 1
cPath = []string{}
} else {
// if we are >=2, descend twice, then generate a new dir, putting us at n-1
cPath = cPath[:len(cPath)-2]
cPath = append(cPath, genDirectoryNameSafe())
}
}
trie.Insert(strings.Join(cPath, "/"), e2etest.PtrOf(true))
trieMu.Unlock()
}
gjm.Wait()
if fc := atomic.LoadInt64(gjm.failureCount); fc > 0 {
return fmt.Errorf("failed generating %d entries", fc)
}
return nil
}
func (m *ManyFoldersGenerator) RegisterFlags(pFlags *pflag.FlagSet) {
pFlags.StringVar(&m.ContainerTarget, FlagContainerName, e2etest.SyntheticContainerManyFoldersSource, "Set a custom container name")
pFlags.StringVar(&m.preferredService, FlagService, "", "Generate against a service other than the default (file)")
}