OpenSlides/performance/cmd/votes.go

224 lines
5.6 KiB
Go

package cmd
import (
"bufio"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/OpenSlides/OpenSlides/performance/client"
"github.com/spf13/cobra"
"github.com/vbauerster/mpb"
)
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.
Example:
performance votes motion --amount 100 --poll_id 42
performance votes assignment --amount 100 --poll_id 42
`
func cmdVotes(cfg *config) *cobra.Command {
cmd := &cobra.Command{
Use: "votes",
Short: "Sends many votes from different users",
ValidArgs: []string{"motion", "assignment", "m", "a"},
Args: cobra.ExactValidArgs(1),
Long: voteHelp,
}
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 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 {
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)
}
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) {
var wgLogin sync.WaitGroup
progress := mpb.New(mpb.WithWaitGroup(&wgLogin))
loginBar := progress.AddBar(int64(len(clients)))
for i := 0; i < len(clients); i++ {
wgLogin.Add(1)
go func(c *client.Client) {
defer wgLogin.Done()
if err := c.Login(); err != nil {
log.Printf("Login failed: %v", err)
return
}
loginBar.Increment()
}(clients[i])
}
progress.Wait()
}
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)))
for i := 0; i < len(clients); i++ {
wgVote.Add(1)
go func(c *client.Client) {
defer wgVote.Done()
req, err := http.NewRequest("POST", url, strings.NewReader(vote))
if err != nil {
log.Printf("Error creating request: %v", err)
return
}
req.Header.Set("Content-Type", "application/json")
if _, err := client.CheckStatus(c.Do(req)); err != nil {
log.Printf("Error sending vote request to %s: %v", url, err)
return
}
voteBar.Increment()
return
}(clients[i])
}
progress.Wait()
}