diff --git a/performance/.gitignore b/performance/.gitignore index 58ad69d6a..cf75d2709 100644 --- a/performance/.gitignore +++ b/performance/.gitignore @@ -1 +1,2 @@ /performance +.vscode diff --git a/performance/client/client.go b/performance/client/client.go index 8aa30a40d..438dd77f6 100644 --- a/performance/client/client.go +++ b/performance/client/client.go @@ -14,7 +14,7 @@ const pathLogin = "/apps/users/login/" // Client can send requests to the server. type Client struct { - domain string + addr string hc *http.Client username, password string @@ -22,9 +22,9 @@ type Client struct { } // 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{ - domain: "https://" + domain, + addr: addr, username: username, password: password, 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 // cookie for later requests. func (c *Client) Login() error { - url := c.domain + pathLogin + url := c.addr + pathLogin payload := fmt.Sprintf(`{"username": "%s", "password": "%s"}`, c.username, c.password) var err error @@ -70,7 +70,7 @@ func (c *Client) Login() 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 { return fmt.Errorf("creating request: %w", err) diff --git a/performance/cmd/browser.go b/performance/cmd/browser.go index e83557d0e..8e5a243d2 100644 --- a/performance/cmd/browser.go +++ b/performance/cmd/browser.go @@ -47,7 +47,7 @@ func cmdBrowser(cfg *config) *cobra.Command { clients := make([]*client.Client, clientCount) 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 { return fmt.Errorf("creating client: %w", err) } @@ -60,7 +60,7 @@ func cmdBrowser(cfg *config) *cobra.Command { wg.Add(1) go func(c *client.Client) { 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) } }(c) @@ -76,7 +76,7 @@ func cmdBrowser(cfg *config) *cobra.Command { 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 { 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) { defer wg.Done() - req, err := http.NewRequest("GET", "https://"+domain+path, nil) + req, err := http.NewRequest("GET", url+path, nil) if err != nil { log.Printf("Error creating request: %v", err) return diff --git a/performance/cmd/connect.go b/performance/cmd/connect.go index ff33f6ef7..e773b477a 100644 --- a/performance/cmd/connect.go +++ b/performance/cmd/connect.go @@ -48,7 +48,7 @@ func cmdConnect(cfg *config) *cobra.Command { 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 { return fmt.Errorf("creating client: %w", err) } @@ -62,7 +62,7 @@ func cmdConnect(cfg *config) *cobra.Command { for i := 0; i < connectionCount; i++ { go func() { - r, err := keepOpen(cfg.domain, c, path) + r, err := keepOpen(cfg.addr(), c, path) if err != nil { log.Println("Can not create connection: %w", err) return @@ -70,7 +70,8 @@ func cmdConnect(cfg *config) *cobra.Command { defer r.Close() 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() { msg, err := scannerAutoupdate(scanner.Text()) if err != nil { @@ -86,7 +87,7 @@ func cmdConnect(cfg *config) *cobra.Command { if errors.Is(err, context.Canceled) { return } - log.Println("Can not read body: %w", err) + log.Printf("Can not read body: %v", err) return } @@ -115,8 +116,8 @@ func cmdConnect(cfg *config) *cobra.Command { return cmd } -func keepOpen(domain string, c *client.Client, path string) (io.ReadCloser, error) { - req, err := http.NewRequest("GET", "https://"+domain+path, nil) +func keepOpen(url string, c *client.Client, path string) (io.ReadCloser, error) { + req, err := http.NewRequest("GET", url+path, nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } diff --git a/performance/cmd/create_amendments.go b/performance/cmd/create_amendments.go index 0a2041597..748102879 100644 --- a/performance/cmd/create_amendments.go +++ b/performance/cmd/create_amendments.go @@ -22,7 +22,7 @@ func cmdCreateAmendments(cfg *config) *cobra.Command { Short: "Creates a motion with amendments.", Long: helpCreateAmendments, 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 { return fmt.Errorf("creating client: %w", err) } @@ -31,15 +31,13 @@ func cmdCreateAmendments(cfg *config) *cobra.Command { return fmt.Errorf("login client: %w", err) } - addr := "https://" + cfg.domain - - motionID, err := createMotion(c, addr, amendmentAmount) + motionID, err := createMotion(c, cfg.addr(), amendmentAmount) if err != nil { return fmt.Errorf("create motion: %w", err) } for i := 0; i < amendmentAmount; i++ { - createAmendment(c, addr, motionID, i) + createAmendment(c, cfg.addr(), motionID, i) } fmt.Println(motionID) diff --git a/performance/cmd/create_candidates.go b/performance/cmd/create_candidates.go new file mode 100644 index 000000000..da4aef891 --- /dev/null +++ b/performance/cmd/create_candidates.go @@ -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) +} diff --git a/performance/cmd/create_users.go b/performance/cmd/create_users.go index a67e58363..48d2817e7 100644 --- a/performance/cmd/create_users.go +++ b/performance/cmd/create_users.go @@ -31,7 +31,7 @@ func cmdCreateUsers(cfg *config) *cobra.Command { Short: "Create a lot of users.", Long: createUsersHelp, 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 { return fmt.Errorf("creating client: %w", err) } @@ -57,7 +57,7 @@ func cmdCreateUsers(cfg *config) *cobra.Command { 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 { return fmt.Errorf("creating mass import request: %w", err) } diff --git a/performance/cmd/root.go b/performance/cmd/root.go index ab98b55e8..4ba478421 100644 --- a/performance/cmd/root.go +++ b/performance/cmd/root.go @@ -12,18 +12,29 @@ type config struct { domain string username 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 { cmd := &cobra.Command{ - Use: "performance", - Short: "performance is a tool that brings OpenSlides to its limit.", - Long: rootHelp, + Use: "performance", + Short: "performance is a tool that brings OpenSlides to its limit.", + Long: rootHelp, + SilenceUsage: true, } 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.password, "password", "p", "admin", "Password to use.") + cmd.PersistentFlags().BoolVar(&cfg.http, "http", false, "Use http instead of https. Default is https.") return cmd } @@ -38,6 +49,7 @@ func Execute() error { cmdCreateUsers(cfg), cmdVotes(cfg), cmdCreateAmendments(cfg), + cmdCreateCandidates(cfg), ) return cmd.Execute() diff --git a/performance/cmd/votes.go b/performance/cmd/votes.go index 9cf4ae9ff..02199909d 100644 --- a/performance/cmd/votes.go +++ b/performance/cmd/votes.go @@ -2,10 +2,13 @@ package cmd import ( "bufio" + "encoding/json" "fmt" + "io" "log" "net/http" "os" + "strconv" "strings" "sync" "time" @@ -20,68 +23,153 @@ const voteHelp = `Sends many votes from different users. This command requires, that there are many user created at the backend. You can use the command "create_users" for this job. -Per default, the command uses the new url to the autoupdate-service. -To test the url from the python-server, you can use the flag "old_url".` +Example: + +performance votes motion --amount 100 --poll_id 42 + +performance votes assignment --amount 100 --poll_id 42 + +` func cmdVotes(cfg *config) *cobra.Command { - var ( - count int - pollID int - oldURL bool - interrupt bool - loginRetry int - ) - cmd := &cobra.Command{ - Use: "votes", - Short: "Sends many votes from different users", - Long: voteHelp, - RunE: func(cmd *cobra.Command, args []string) error { - url := "https://%s/system/vote/motion/%d" - 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) - }, + Use: "votes", + Short: "Sends many votes from different users", + ValidArgs: []string{"motion", "assignment", "m", "a"}, + Args: cobra.ExactValidArgs(1), + Long: voteHelp, } - cmd.Flags().IntVarP(&count, "amount", "a", 10, "Amount of users to use.") - cmd.Flags().IntVarP(&pollID, "poll_id", "i", 1, "ID of the poll to use.") - cmd.Flags().BoolVarP(&oldURL, "old_url", "o", false, "Use old url to python.") - cmd.Flags().BoolVar(&interrupt, "interrupt", false, "Wait for a user input after login.") - cmd.Flags().IntVarP(&loginRetry, "login_retry", "r", 3, "Retries send login requests before returning an error.") + amount := cmd.Flags().IntP("amount", "n", 10, "Amount of users to use.") + pollID := cmd.Flags().IntP("poll_id", "i", 1, "ID of the poll to use.") + interrupt := cmd.Flags().Bool("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.") + 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 } -func sendVotes(url string, domain string, count int, interrupt bool, loginRetry int) error { - var clients []*client.Client - for i := 0; i < count; i++ { - c, err := client.New(domain, fmt.Sprintf("dummy%d", i+1), "pass", client.WithLoginRetry(loginRetry)) +func assignmentPollOptions(addr string, pollID int) ([]int, error) { + c, err := client.New(addr, "dummy1", "pass") + if err != nil { + 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 { - 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) - 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") + // 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) } - start = time.Now() - massVotes(clients, url) - log.Printf("All Clients have voted in %v", time.Now().Sub(start)) - return nil + 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) + } + + 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) { @@ -105,7 +193,7 @@ func massLogin(clients []*client.Client, tries int) { progress.Wait() } -func massVotes(clients []*client.Client, url string) { +func massVotes(clients []*client.Client, url string, vote string) { var wgVote sync.WaitGroup progress := mpb.New(mpb.WithWaitGroup(&wgVote)) voteBar := progress.AddBar(int64(len(clients))) @@ -114,8 +202,7 @@ func massVotes(clients []*client.Client, url string) { go func(c *client.Client) { defer wgVote.Done() - body := `{"data":"Y"}` - req, err := http.NewRequest("POST", url, strings.NewReader(body)) + req, err := http.NewRequest("POST", url, strings.NewReader(vote)) if err != nil { log.Printf("Error creating request: %v", err) return @@ -124,7 +211,7 @@ func massVotes(clients []*client.Client, url string) { req.Header.Set("Content-Type", "application/json") 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 }