Merge pull request #6056 from ostcar/performance_assignment_poll

Performance assignment poll
This commit is contained in:
Emanuel Schütze 2021-06-17 13:11:12 +02:00 committed by GitHub
commit 382fcf4a67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 305 additions and 76 deletions

View File

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

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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)

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.",
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)
}

View File

@ -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()

View File

@ -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
}