performance test for assinment polls

This commit is contained in:
Oskar Hahn 2021-05-07 15:43:43 +02:00 committed by Emanuel Schütze
parent 8d73f08072
commit 9a9ef8e039
9 changed files with 305 additions and 76 deletions

View File

@ -1 +1,2 @@
/performance /performance
.vscode

View File

@ -14,7 +14,7 @@ const pathLogin = "/apps/users/login/"
// Client can send requests to the server. // Client can send requests to the server.
type Client struct { type Client struct {
domain string addr string
hc *http.Client hc *http.Client
username, password string username, password string
@ -22,9 +22,9 @@ type Client struct {
} }
// New creates a client object. No requests are sent. // New creates a client object. No requests are sent.
func New(domain, username, password string, options ...Option) (*Client, error) { func New(addr, username, password string, options ...Option) (*Client, error) {
c := &Client{ c := &Client{
domain: "https://" + domain, addr: addr,
username: username, username: username,
password: password, password: password,
loginRetry: 1, loginRetry: 1,
@ -51,7 +51,7 @@ func New(domain, username, password string, options ...Option) (*Client, error)
// Login uses the username and password to login the client. Sets the returned // Login uses the username and password to login the client. Sets the returned
// cookie for later requests. // cookie for later requests.
func (c *Client) Login() error { func (c *Client) Login() error {
url := c.domain + pathLogin url := c.addr + pathLogin
payload := fmt.Sprintf(`{"username": "%s", "password": "%s"}`, c.username, c.password) payload := fmt.Sprintf(`{"username": "%s", "password": "%s"}`, c.username, c.password)
var err error var err error
@ -70,7 +70,7 @@ func (c *Client) Login() error {
} }
func (c *Client) get(ctx context.Context, path string) error { func (c *Client) get(ctx context.Context, path string) error {
req, err := http.NewRequestWithContext(ctx, "GET", c.domain+path, nil) req, err := http.NewRequestWithContext(ctx, "GET", c.addr+path, nil)
if err != nil { if err != nil {
return fmt.Errorf("creating request: %w", err) return fmt.Errorf("creating request: %w", err)

View File

@ -47,7 +47,7 @@ func cmdBrowser(cfg *config) *cobra.Command {
clients := make([]*client.Client, clientCount) clients := make([]*client.Client, clientCount)
for i := range clients { for i := range clients {
c, err := client.New(cfg.domain, cfg.username, cfg.password) c, err := client.New(cfg.addr(), cfg.username, cfg.password)
if err != nil { if err != nil {
return fmt.Errorf("creating client: %w", err) return fmt.Errorf("creating client: %w", err)
} }
@ -60,7 +60,7 @@ func cmdBrowser(cfg *config) *cobra.Command {
wg.Add(1) wg.Add(1)
go func(c *client.Client) { go func(c *client.Client) {
defer wg.Done() defer wg.Done()
if err := browser(cfg.domain, c, bar); err != nil { if err := browser(cfg.addr(), c, bar); err != nil {
log.Printf("Client failed: %v", err) log.Printf("Client failed: %v", err)
} }
}(c) }(c)
@ -76,7 +76,7 @@ func cmdBrowser(cfg *config) *cobra.Command {
return cmd return cmd
} }
func browser(domain string, c *client.Client, bar *mpb.Bar) error { func browser(url string, c *client.Client, bar *mpb.Bar) error {
if err := c.Login(); err != nil { if err := c.Login(); err != nil {
return fmt.Errorf("login client: %w", err) return fmt.Errorf("login client: %w", err)
} }
@ -94,7 +94,7 @@ func browser(domain string, c *client.Client, bar *mpb.Bar) error {
go func(path string) { go func(path string) {
defer wg.Done() defer wg.Done()
req, err := http.NewRequest("GET", "https://"+domain+path, nil) req, err := http.NewRequest("GET", url+path, nil)
if err != nil { if err != nil {
log.Printf("Error creating request: %v", err) log.Printf("Error creating request: %v", err)
return return

View File

@ -48,7 +48,7 @@ func cmdConnect(cfg *config) *cobra.Command {
path := "/system/autoupdate" path := "/system/autoupdate"
c, err := client.New(cfg.domain, cfg.username, cfg.password) c, err := client.New(cfg.addr(), cfg.username, cfg.password)
if err != nil { if err != nil {
return fmt.Errorf("creating client: %w", err) return fmt.Errorf("creating client: %w", err)
} }
@ -62,7 +62,7 @@ func cmdConnect(cfg *config) *cobra.Command {
for i := 0; i < connectionCount; i++ { for i := 0; i < connectionCount; i++ {
go func() { go func() {
r, err := keepOpen(cfg.domain, c, path) r, err := keepOpen(cfg.addr(), c, path)
if err != nil { if err != nil {
log.Println("Can not create connection: %w", err) log.Println("Can not create connection: %w", err)
return return
@ -70,7 +70,8 @@ func cmdConnect(cfg *config) *cobra.Command {
defer r.Close() defer r.Close()
scanner := bufio.NewScanner(r) scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 10), 1_000_000) const MB = 1 << 20
scanner.Buffer(make([]byte, 10), 16*MB)
for scanner.Scan() { for scanner.Scan() {
msg, err := scannerAutoupdate(scanner.Text()) msg, err := scannerAutoupdate(scanner.Text())
if err != nil { if err != nil {
@ -86,7 +87,7 @@ func cmdConnect(cfg *config) *cobra.Command {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return return
} }
log.Println("Can not read body: %w", err) log.Printf("Can not read body: %v", err)
return return
} }
@ -115,8 +116,8 @@ func cmdConnect(cfg *config) *cobra.Command {
return cmd return cmd
} }
func keepOpen(domain string, c *client.Client, path string) (io.ReadCloser, error) { func keepOpen(url string, c *client.Client, path string) (io.ReadCloser, error) {
req, err := http.NewRequest("GET", "https://"+domain+path, nil) req, err := http.NewRequest("GET", url+path, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("creating request: %w", err) return nil, fmt.Errorf("creating request: %w", err)
} }

View File

@ -22,7 +22,7 @@ func cmdCreateAmendments(cfg *config) *cobra.Command {
Short: "Creates a motion with amendments.", Short: "Creates a motion with amendments.",
Long: helpCreateAmendments, Long: helpCreateAmendments,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
c, err := client.New(cfg.domain, cfg.username, cfg.password) c, err := client.New(cfg.addr(), cfg.username, cfg.password)
if err != nil { if err != nil {
return fmt.Errorf("creating client: %w", err) return fmt.Errorf("creating client: %w", err)
} }
@ -31,15 +31,13 @@ func cmdCreateAmendments(cfg *config) *cobra.Command {
return fmt.Errorf("login client: %w", err) return fmt.Errorf("login client: %w", err)
} }
addr := "https://" + cfg.domain motionID, err := createMotion(c, cfg.addr(), amendmentAmount)
motionID, err := createMotion(c, addr, amendmentAmount)
if err != nil { if err != nil {
return fmt.Errorf("create motion: %w", err) return fmt.Errorf("create motion: %w", err)
} }
for i := 0; i < amendmentAmount; i++ { for i := 0; i < amendmentAmount; i++ {
createAmendment(c, addr, motionID, i) createAmendment(c, cfg.addr(), motionID, i)
} }
fmt.Println(motionID) fmt.Println(motionID)

View File

@ -0,0 +1,130 @@
package cmd
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/OpenSlides/OpenSlides/performance/client"
"github.com/spf13/cobra"
)
const helpCreateCandidates = `Creates candidates to an assignment
This command can be used to test assignment votes.
`
func cmdCreateCandidates(cfg *config) *cobra.Command {
cmd := &cobra.Command{
Use: "create_candidates",
Short: "Creates candidates to an assignment",
Long: helpCreateCandidates,
}
amount := cmd.Flags().IntP("amount", "a", 10, "amount of candidates to add")
assignmentID := cmd.Flags().Int("assignment_id", 0, "assignment where the candidates are created")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
c, err := client.New(cfg.addr(), cfg.username, cfg.password)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
if err := c.Login(); err != nil {
return fmt.Errorf("login client: %w", err)
}
uids, err := userIDs(cfg, c, *amount)
if err != nil {
return fmt.Errorf("getting userIDs: %w", err)
}
for _, id := range uids {
if err := createCandidate(c, cfg.addr(), *assignmentID, id); err != nil {
return fmt.Errorf("create candidate for uid %d: %w", id, err)
}
}
return nil
}
return cmd
}
func createCandidate(c *client.Client, addr string, assignmentID int, userID int) error {
req, err := http.NewRequest(
"POST",
fmt.Sprintf("%s/rest/assignments/assignment/%d/candidature_other/", addr, assignmentID),
strings.NewReader(fmt.Sprintf(`{"user":%d}`, userID)),
)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
if _, err := client.CheckStatus(c.Do(req)); err != nil {
return fmt.Errorf("sending request: %w", err)
}
return nil
}
func userIDs(cfg *config, c *client.Client, amount int) ([]int, error) {
req, err := http.NewRequest("GET", fmt.Sprintf("%s/system/autoupdate", cfg.addr()), nil)
if err != nil {
return nil, fmt.Errorf("creating fetching all request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, fmt.Errorf("sending fetch all request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, err := io.ReadAll(resp.Body)
if err != nil {
body = []byte(fmt.Sprintf("[can not read body: %v", err))
}
return nil, fmt.Errorf("got status %s: %s", resp.Status, body)
}
// First message is a connection info. Throw it away.
var devNull json.RawMessage
if err := json.NewDecoder(resp.Body).Decode(&devNull); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
var content struct {
Changed map[string][]json.RawMessage `json:"changed"`
}
if err := json.NewDecoder(resp.Body).Decode(&content); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
users := content.Changed["users/user"]
if len(users) == 0 {
return nil, fmt.Errorf("no users found")
}
var ids []int
for i, u := range users {
var user struct {
ID int `json:"id"`
UserName string `json:"username"`
}
if err := json.Unmarshal(u, &user); err != nil {
return nil, fmt.Errorf("decoding %dth user: %w", i, err)
}
if !strings.HasPrefix(user.UserName, "dummy") {
continue
}
ids = append(ids, user.ID)
if len(ids) >= amount {
return ids, nil
}
}
return nil, fmt.Errorf("could not find %d dummy users", amount)
}

View File

@ -31,7 +31,7 @@ func cmdCreateUsers(cfg *config) *cobra.Command {
Short: "Create a lot of users.", Short: "Create a lot of users.",
Long: createUsersHelp, Long: createUsersHelp,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
c, err := client.New(cfg.domain, cfg.username, cfg.password) c, err := client.New(cfg.addr(), cfg.username, cfg.password)
if err != nil { if err != nil {
return fmt.Errorf("creating client: %w", err) return fmt.Errorf("creating client: %w", err)
} }
@ -57,7 +57,7 @@ func cmdCreateUsers(cfg *config) *cobra.Command {
return fmt.Errorf("encoding users: %w", err) return fmt.Errorf("encoding users: %w", err)
} }
req, err := http.NewRequest("POST", "https://"+cfg.domain+"/rest/users/user/mass_import/", bytes.NewReader(bs)) req, err := http.NewRequest("POST", cfg.addr()+"/rest/users/user/mass_import/", bytes.NewReader(bs))
if err != nil { if err != nil {
return fmt.Errorf("creating mass import request: %w", err) return fmt.Errorf("creating mass import request: %w", err)
} }

View File

@ -12,18 +12,29 @@ type config struct {
domain string domain string
username string username string
password string password string
http bool
}
func (c *config) addr() string {
proto := "https"
if c.http {
proto = "http"
}
return proto + "://" + c.domain
} }
func cmdRoot(cfg *config) *cobra.Command { func cmdRoot(cfg *config) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "performance", Use: "performance",
Short: "performance is a tool that brings OpenSlides to its limit.", Short: "performance is a tool that brings OpenSlides to its limit.",
Long: rootHelp, Long: rootHelp,
SilenceUsage: true,
} }
cmd.PersistentFlags().StringVarP(&cfg.domain, "domain", "d", "localhost:8000", "Domain where to send the requests") cmd.PersistentFlags().StringVarP(&cfg.domain, "domain", "d", "localhost:8000", "Domain where to send the requests")
cmd.PersistentFlags().StringVarP(&cfg.username, "username", "u", "admin", "Username that can create the users.") cmd.PersistentFlags().StringVarP(&cfg.username, "username", "u", "admin", "Username that can create the users.")
cmd.PersistentFlags().StringVarP(&cfg.password, "password", "p", "admin", "Password to use.") cmd.PersistentFlags().StringVarP(&cfg.password, "password", "p", "admin", "Password to use.")
cmd.PersistentFlags().BoolVar(&cfg.http, "http", false, "Use http instead of https. Default is https.")
return cmd return cmd
} }
@ -38,6 +49,7 @@ func Execute() error {
cmdCreateUsers(cfg), cmdCreateUsers(cfg),
cmdVotes(cfg), cmdVotes(cfg),
cmdCreateAmendments(cfg), cmdCreateAmendments(cfg),
cmdCreateCandidates(cfg),
) )
return cmd.Execute() return cmd.Execute()

View File

@ -2,10 +2,13 @@ package cmd
import ( import (
"bufio" "bufio"
"encoding/json"
"fmt" "fmt"
"io"
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -20,68 +23,153 @@ const voteHelp = `Sends many votes from different users.
This command requires, that there are many user created at the This command requires, that there are many user created at the
backend. You can use the command "create_users" for this job. backend. You can use the command "create_users" for this job.
Per default, the command uses the new url to the autoupdate-service. Example:
To test the url from the python-server, you can use the flag "old_url".`
performance votes motion --amount 100 --poll_id 42
performance votes assignment --amount 100 --poll_id 42
`
func cmdVotes(cfg *config) *cobra.Command { func cmdVotes(cfg *config) *cobra.Command {
var (
count int
pollID int
oldURL bool
interrupt bool
loginRetry int
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "votes", Use: "votes",
Short: "Sends many votes from different users", Short: "Sends many votes from different users",
Long: voteHelp, ValidArgs: []string{"motion", "assignment", "m", "a"},
RunE: func(cmd *cobra.Command, args []string) error { Args: cobra.ExactValidArgs(1),
url := "https://%s/system/vote/motion/%d" Long: voteHelp,
if oldURL {
url = "https://%s/rest/motions/motion-poll/%d/vote/"
}
url = fmt.Sprintf(url, cfg.domain, pollID)
return sendVotes(url, cfg.domain, count, interrupt, loginRetry)
},
} }
cmd.Flags().IntVarP(&count, "amount", "a", 10, "Amount of users to use.") amount := cmd.Flags().IntP("amount", "n", 10, "Amount of users to use.")
cmd.Flags().IntVarP(&pollID, "poll_id", "i", 1, "ID of the poll to use.") pollID := cmd.Flags().IntP("poll_id", "i", 1, "ID of the poll to use.")
cmd.Flags().BoolVarP(&oldURL, "old_url", "o", false, "Use old url to python.") interrupt := cmd.Flags().Bool("interrupt", false, "Wait for a user input after login.")
cmd.Flags().BoolVar(&interrupt, "interrupt", false, "Wait for a user input after login.") loginRetry := cmd.Flags().IntP("login_retry", "r", 3, "Retries send login requests before returning an error.")
cmd.Flags().IntVarP(&loginRetry, "login_retry", "r", 3, "Retries send login requests before returning an error.") choice := cmd.Flags().IntP("choice", "c", 0, "Amount of answers per vote.")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
var assignment bool
if args[0] == "assignment" || args[0] == "a" {
assignment = true
}
url := "%s/rest/motions/motion-poll/%d/vote/"
vote := `{"data":"Y"}`
if assignment {
url = "%s/rest/assignments/assignment-poll/%d/vote/"
options, err := assignmentPollOptions(cfg.addr(), *pollID)
if err != nil {
return fmt.Errorf("fetch options: %w", err)
}
votedata := make(map[string]int)
for i, o := range options {
if *choice != 0 && i >= *choice {
break
}
votedata[strconv.Itoa(o)] = 1
}
decoded, err := json.Marshal(votedata)
if err != nil {
return fmt.Errorf("decoding assignment option data: %w", err)
}
vote = fmt.Sprintf(`{"data":%s}`, decoded)
}
url = fmt.Sprintf(url, cfg.addr(), *pollID)
var clients []*client.Client
for i := 0; i < *amount; i++ {
c, err := client.New(cfg.addr(), fmt.Sprintf("dummy%d", i+1), "pass", client.WithLoginRetry(*loginRetry))
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
clients = append(clients, c)
}
log.Printf("Login %d clients", *amount)
start := time.Now()
massLogin(clients, *loginRetry)
log.Printf("All clients logged in %v", time.Now().Sub(start))
if *interrupt {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Hit enter to continue")
reader.ReadString('\n')
log.Println("Starting voting")
}
start = time.Now()
massVotes(clients, url, vote)
log.Printf("All Clients have voted in %v", time.Now().Sub(start))
return nil
}
return cmd return cmd
} }
func sendVotes(url string, domain string, count int, interrupt bool, loginRetry int) error { func assignmentPollOptions(addr string, pollID int) ([]int, error) {
var clients []*client.Client c, err := client.New(addr, "dummy1", "pass")
for i := 0; i < count; i++ { if err != nil {
c, err := client.New(domain, fmt.Sprintf("dummy%d", i+1), "pass", client.WithLoginRetry(loginRetry)) return nil, fmt.Errorf("creating admin client: %w", err)
}
if err := c.Login(); err != nil {
return nil, fmt.Errorf("login admin client: %w", err)
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/system/autoupdate", addr), nil)
if err != nil {
return nil, fmt.Errorf("creating fetching all request: %w", err)
}
resp, err := c.Do(req)
if err != nil {
return nil, fmt.Errorf("sending fetch all request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return fmt.Errorf("creating client: %w", err) body = []byte(fmt.Sprintf("[can not read body: %v", err))
} }
clients = append(clients, c) return nil, fmt.Errorf("got status %s: %s", resp.Status, body)
} }
log.Printf("Login %d clients", count) // First message is a connection info. Throw it away.
start := time.Now() var devNull json.RawMessage
massLogin(clients, loginRetry) if err := json.NewDecoder(resp.Body).Decode(&devNull); err != nil {
log.Printf("All clients logged in %v", time.Now().Sub(start)) return nil, fmt.Errorf("decoding response: %w", err)
if interrupt {
reader := bufio.NewReader(os.Stdin)
fmt.Println("Hit enter to continue")
reader.ReadString('\n')
log.Println("Starting voting")
} }
start = time.Now() var content struct {
massVotes(clients, url) Changed map[string][]json.RawMessage `json:"changed"`
log.Printf("All Clients have voted in %v", time.Now().Sub(start)) }
return nil if err := json.NewDecoder(resp.Body).Decode(&content); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
polls := content.Changed["assignments/assignment-poll"]
if len(polls) == 0 {
return nil, fmt.Errorf("no polls found")
}
for i, p := range polls {
var poll struct {
ID int `json:"id"`
OptionIDs []int `json:"options_id"`
}
if err := json.Unmarshal(p, &poll); err != nil {
return nil, fmt.Errorf("decoding %dth poll: %w", i, err)
}
if poll.ID != pollID {
continue
}
return poll.OptionIDs, nil
}
return nil, fmt.Errorf("no assignment-poll with id %d", pollID)
} }
func massLogin(clients []*client.Client, tries int) { func massLogin(clients []*client.Client, tries int) {
@ -105,7 +193,7 @@ func massLogin(clients []*client.Client, tries int) {
progress.Wait() progress.Wait()
} }
func massVotes(clients []*client.Client, url string) { func massVotes(clients []*client.Client, url string, vote string) {
var wgVote sync.WaitGroup var wgVote sync.WaitGroup
progress := mpb.New(mpb.WithWaitGroup(&wgVote)) progress := mpb.New(mpb.WithWaitGroup(&wgVote))
voteBar := progress.AddBar(int64(len(clients))) voteBar := progress.AddBar(int64(len(clients)))
@ -114,8 +202,7 @@ func massVotes(clients []*client.Client, url string) {
go func(c *client.Client) { go func(c *client.Client) {
defer wgVote.Done() defer wgVote.Done()
body := `{"data":"Y"}` req, err := http.NewRequest("POST", url, strings.NewReader(vote))
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil { if err != nil {
log.Printf("Error creating request: %v", err) log.Printf("Error creating request: %v", err)
return return
@ -124,7 +211,7 @@ func massVotes(clients []*client.Client, url string) {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
if _, err := client.CheckStatus(c.Do(req)); err != nil { if _, err := client.CheckStatus(c.Do(req)); err != nil {
log.Printf("Error sending vote request: %v", err) log.Printf("Error sending vote request to %s: %v", url, err)
return return
} }