github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/loadtest/loader.go (about) 1 package loadtest 2 3 import ( 4 "context" 5 "encoding/base64" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "time" 12 13 "github.com/deepmap/oapi-codegen/pkg/securityprovider" 14 "github.com/google/uuid" 15 "github.com/jedib0t/go-pretty/v6/text" 16 "github.com/schollz/progressbar/v3" 17 "github.com/treeverse/lakefs/pkg/api/apigen" 18 "github.com/treeverse/lakefs/pkg/api/apiutil" 19 "github.com/treeverse/lakefs/pkg/auth/model" 20 "github.com/treeverse/lakefs/pkg/logging" 21 "github.com/treeverse/lakefs/pkg/version" 22 vegeta "github.com/tsenart/vegeta/v12/lib" 23 ) 24 25 type Loader struct { 26 Reader *io.PipeReader 27 Writer *io.PipeWriter 28 Config Config 29 NewRepoName string 30 Metrics map[string]*vegeta.Metrics 31 TotalMetrics *vegeta.Metrics 32 } 33 34 type Config struct { 35 FreqPerSecond int 36 Duration time.Duration 37 MaxWorkers uint64 38 RepoName string 39 StorageNamespace string 40 KeepRepo bool 41 Credentials model.Credential 42 ServerAddress string 43 ShowProgress bool 44 } 45 46 var ( 47 ErrCreateClient = errors.New("failed to create lakeFS client") 48 ErrTestErrors = errors.New("got errors during loadtest, see output for details") 49 ErrRepositoryCreateFailed = errors.New("repository create failed") 50 ErrRepositoryDeleteFailed = errors.New("repository delete failed") 51 ) 52 53 func NewLoader(config Config) *Loader { 54 reader, writer := io.Pipe() 55 res := &Loader{ 56 Config: config, 57 Reader: reader, 58 Writer: writer, 59 } 60 return res 61 } 62 63 func (t *Loader) Run() error { 64 apiClient, err := t.getClient() 65 if err != nil { 66 return err 67 } 68 repoName, err := t.createRepo(apiClient) 69 if err != nil { 70 return err 71 } 72 stopCh := make(chan struct{}) 73 if t.Config.ShowProgress { 74 progressBar(t.Config.Duration) 75 } 76 out := new(SimpleScenario).Play(t.Config.ServerAddress, repoName, stopCh) 77 errs := t.streamRequests(out) 78 hasErrors := t.doAttack() 79 close(stopCh) 80 _ = t.Writer.Close() 81 _ = t.Reader.Close() 82 if t.Config.RepoName == "" && !t.Config.KeepRepo { 83 ctx := context.Background() 84 resp, err := apiClient.DeleteRepositoryWithResponse(ctx, t.NewRepoName, &apigen.DeleteRepositoryParams{}) 85 if err != nil { 86 return err 87 } 88 if resp.HTTPResponse.StatusCode != http.StatusNoContent { 89 return fmt.Errorf("%w: %s (%d)", ErrRepositoryDeleteFailed, resp.HTTPResponse.Status, resp.HTTPResponse.StatusCode) 90 } 91 } 92 for err := range errs { 93 if errors.Is(err, io.ErrClosedPipe) { 94 continue 95 } 96 logging.ContextUnavailable().WithError(err).Error("error during request pipeline") 97 return err 98 } 99 err = printResults(t.Metrics, t.TotalMetrics) 100 if err != nil { 101 return err 102 } 103 if hasErrors { 104 return ErrTestErrors 105 } 106 return nil 107 } 108 109 func (t *Loader) createRepo(apiClient apigen.ClientWithResponsesInterface) (string, error) { 110 if t.Config.RepoName != "" { 111 // using an existing repo, no need to create one 112 return t.Config.RepoName, nil 113 } 114 t.NewRepoName = uuid.New().String() 115 ctx := context.Background() 116 resp, err := apiClient.CreateRepositoryWithResponse(ctx, &apigen.CreateRepositoryParams{}, apigen.CreateRepositoryJSONRequestBody{ 117 DefaultBranch: apiutil.Ptr("main"), 118 Name: t.NewRepoName, 119 StorageNamespace: t.Config.StorageNamespace, 120 }) 121 if err != nil { 122 return "", fmt.Errorf("failed to create lakeFS repository '%s' (%s): %w", t.NewRepoName, t.Config.StorageNamespace, err) 123 } 124 if resp.HTTPResponse.StatusCode != http.StatusCreated { 125 return "", fmt.Errorf("%w: %s (%d)", ErrRepositoryCreateFailed, resp.HTTPResponse.Status, resp.HTTPResponse.StatusCode) 126 } 127 return t.NewRepoName, nil 128 } 129 130 func (t *Loader) getClient() (apigen.ClientWithResponsesInterface, error) { 131 if t.Config.RepoName != "" { 132 // using an existing repo, no need to create a client 133 return nil, nil 134 } 135 basicAuthProvider, err := securityprovider.NewSecurityProviderBasicAuth(t.Config.Credentials.AccessKeyID, t.Config.Credentials.SecretAccessKey) 136 if err != nil { 137 return nil, err 138 } 139 140 serverEndpoint := t.Config.ServerAddress + apiutil.BaseURL 141 apiClient, err := apigen.NewClientWithResponses( 142 serverEndpoint, 143 apigen.WithRequestEditorFn(basicAuthProvider.Intercept), 144 apigen.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { 145 req.Header.Set("User-Agent", "lakefs-loadtest/"+version.Version) 146 return nil 147 }), 148 ) 149 if err != nil { 150 return nil, ErrCreateClient 151 } 152 return apiClient, nil 153 } 154 155 func (t *Loader) doAttack() (hasErrors bool) { 156 targeter := vegeta.NewJSONTargeter(t.Reader, nil, 157 http.Header{"Authorization": []string{"Basic " + getAuth(&t.Config.Credentials)}}) 158 attacker := vegeta.NewAttacker(vegeta.MaxWorkers(t.Config.MaxWorkers)) 159 t.Metrics = make(map[string]*vegeta.Metrics) 160 t.TotalMetrics = new(vegeta.Metrics) 161 rate := vegeta.Rate{Freq: t.Config.FreqPerSecond, Per: time.Second} 162 for res := range attacker.Attack(targeter, rate, t.Config.Duration, "lakeFS loadtest test") { 163 typ := GetRequestType(*res) 164 if len(res.Error) > 0 { 165 logging.ContextUnavailable().Debugf("Error in request type %s, error: %s, status: %d", typ, res.Error, res.Code) 166 hasErrors = true 167 } 168 typeMetrics := t.Metrics[typ] 169 if typeMetrics == nil { 170 typeMetrics = new(vegeta.Metrics) 171 t.Metrics[typ] = typeMetrics 172 } 173 typeMetrics.Add(res) 174 t.TotalMetrics.Add(res) 175 } 176 return 177 } 178 179 func progressBar(duration time.Duration) { 180 durationInSec := int(duration.Seconds()) 181 progress := progressbar.NewOptions(durationInSec, progressbar.OptionSetPredictTime(false), progressbar.OptionFullWidth()) 182 go func() { 183 for i := 0; i < durationInSec; i++ { 184 _ = progress.Add(1) 185 time.Sleep(time.Second) 186 } 187 _ = progress.Clear() 188 }() 189 } 190 191 func printResults(metrics map[string]*vegeta.Metrics, metricsTotal *vegeta.Metrics) error { 192 for requestType, typeMetrics := range metrics { 193 typeMetrics.Close() 194 fmt.Println(text.FgYellow.Sprintf("Results for request type: %s", requestType)) 195 196 err := vegeta.NewTextReporter(typeMetrics).Report(os.Stdout) 197 if err != nil { 198 fmt.Println("Error trying to write report") 199 return err 200 } 201 fmt.Println() 202 } 203 fmt.Println(text.FgYellow.Sprintf("Results for ALL requests combined:")) 204 metricsTotal.Close() 205 err := vegeta.NewTextReporter(metricsTotal).Report(os.Stdout) 206 if err != nil { 207 fmt.Println("Error trying to write report") 208 return err 209 } 210 return nil 211 } 212 213 func (t *Loader) streamRequests(in <-chan vegeta.Target) <-chan error { 214 errs := make(chan error, 1) 215 encoder := vegeta.NewJSONTargetEncoder(t.Writer) 216 go func() { 217 defer close(errs) 218 for tgt := range in { 219 tgt := tgt // pin 220 err := encoder.Encode(&tgt) 221 if err != nil { 222 errs <- err 223 return 224 } 225 } 226 }() 227 return errs 228 } 229 230 func getAuth(credentials *model.Credential) string { 231 return base64.StdEncoding.EncodeToString([]byte(credentials.AccessKeyID + ":" + credentials.SecretAccessKey)) 232 }