Files
httprunner/hrp/pkg/boomer/boomer.go
2022-10-11 11:17:58 +08:00

614 lines
17 KiB
Go

package boomer
import (
"math"
"os"
"os/signal"
"syscall"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"golang.org/x/net/context"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
// Mode is the running mode of boomer, both standalone and distributed are supported.
type Mode int
const (
// DistributedMasterMode requires being connected by each worker.
DistributedMasterMode Mode = iota
// DistributedWorkerMode requires connecting to a master.
DistributedWorkerMode
// StandaloneMode will run without a master.
StandaloneMode
)
// A Boomer is used to run tasks.
type Boomer struct {
masterHost string
masterPort int
mode Mode
localRunner *localRunner
workerRunner *workerRunner
masterRunner *masterRunner
testcasePath []string
cpuProfile string
cpuProfileDuration time.Duration
memoryProfile string
memoryProfileDuration time.Duration
disableKeepalive bool
disableCompression bool
}
type Profile struct {
SpawnCount int64 `json:"spawn-count,omitempty" yaml:"spawn-count,omitempty" mapstructure:"spawn-count,omitempty"`
SpawnRate float64 `json:"spawn-rate,omitempty" yaml:"spawn-rate,omitempty" mapstructure:"spawn-rate,omitempty"`
RunTime int64 `json:"run-time,omitempty" yaml:"run-time,omitempty" mapstructure:"run-time,omitempty"`
MaxRPS int64 `json:"max-rps,omitempty" yaml:"max-rps,omitempty" mapstructure:"max-rps,omitempty"`
LoopCount int64 `json:"loop-count,omitempty" yaml:"loop-count,omitempty" mapstructure:"loop-count,omitempty"`
RequestIncreaseRate string `json:"request-increase-rate,omitempty" yaml:"request-increase-rate,omitempty" mapstructure:"request-increase-rate,omitempty"`
MemoryProfile string `json:"memory-profile,omitempty" yaml:"memory-profile,omitempty" mapstructure:"memory-profile,omitempty"`
MemoryProfileDuration time.Duration `json:"memory-profile-duration,omitempty" yaml:"memory-profile-duration,omitempty" mapstructure:"memory-profile-duration,omitempty"`
CPUProfile string `json:"cpu-profile,omitempty" yaml:"cpu-profile,omitempty" mapstructure:"cpu-profile,omitempty"`
CPUProfileDuration time.Duration `json:"cpu-profile-duration,omitempty" yaml:"cpu-profile-duration,omitempty" mapstructure:"cpu-profile-duration,omitempty"`
PrometheusPushgatewayURL string `json:"prometheus-gateway,omitempty" yaml:"prometheus-gateway,omitempty" mapstructure:"prometheus-gateway,omitempty"`
DisableConsoleOutput bool `json:"disable-console-output,omitempty" yaml:"disable-console-output,omitempty" mapstructure:"disable-console-output,omitempty"`
DisableCompression bool `json:"disable-compression,omitempty" yaml:"disable-compression,omitempty" mapstructure:"disable-compression,omitempty"`
DisableKeepalive bool `json:"disable-keepalive,omitempty" yaml:"disable-keepalive,omitempty" mapstructure:"disable-keepalive,omitempty"`
}
func NewProfile() *Profile {
return &Profile{
SpawnCount: 1,
SpawnRate: 1,
MaxRPS: -1,
LoopCount: -1,
RequestIncreaseRate: "-1",
CPUProfileDuration: 30 * time.Second,
MemoryProfileDuration: 30 * time.Second,
}
}
func (b *Boomer) GetProfile() *Profile {
switch b.mode {
case DistributedMasterMode:
return b.masterRunner.profile
case DistributedWorkerMode:
return b.workerRunner.profile
default:
return b.localRunner.profile
}
}
func (b *Boomer) SetProfile(profile *Profile) {
switch b.mode {
case DistributedMasterMode:
b.masterRunner.profile = profile
case DistributedWorkerMode:
b.workerRunner.profile = profile
default:
b.localRunner.profile = profile
}
}
// SetMode only accepts boomer.DistributedMasterMode、boomer.DistributedWorkerMode and boomer.StandaloneMode.
func (b *Boomer) SetMode(mode Mode) {
switch mode {
case DistributedMasterMode:
b.mode = DistributedMasterMode
case DistributedWorkerMode:
b.mode = DistributedWorkerMode
case StandaloneMode:
b.mode = StandaloneMode
default:
log.Error().Err(errors.New("Invalid mode, ignored!"))
}
}
// GetMode returns boomer operating mode
func (b *Boomer) GetMode() string {
switch b.mode {
case DistributedMasterMode:
return "master"
case DistributedWorkerMode:
return "worker"
case StandaloneMode:
return "standalone"
default:
log.Error().Err(errors.New("Invalid mode, ignored!"))
return ""
}
}
// NewStandaloneBoomer returns a new Boomer, which can run without master.
func NewStandaloneBoomer(spawnCount int64, spawnRate float64) *Boomer {
return &Boomer{
mode: StandaloneMode,
localRunner: newLocalRunner(spawnCount, spawnRate),
}
}
// NewMasterBoomer returns a new Boomer.
func NewMasterBoomer(masterBindHost string, masterBindPort int) *Boomer {
return &Boomer{
masterRunner: newMasterRunner(masterBindHost, masterBindPort),
mode: DistributedMasterMode,
}
}
// NewWorkerBoomer returns a new Boomer.
func NewWorkerBoomer(masterHost string, masterPort int) *Boomer {
return &Boomer{
workerRunner: newWorkerRunner(masterHost, masterPort),
masterHost: masterHost,
masterPort: masterPort,
mode: DistributedWorkerMode,
}
}
// SetAutoStart auto start to load testing
func (b *Boomer) SetAutoStart() {
b.masterRunner.autoStart = true
}
// RunMaster start to run master runner
func (b *Boomer) RunMaster() {
b.masterRunner.run()
}
// RunWorker start to run worker runner
func (b *Boomer) RunWorker() {
b.workerRunner.run()
}
// TestCaseBytesChan gets test case bytes chan
func (b *Boomer) TestCaseBytesChan() chan []byte {
return b.masterRunner.testCaseBytesChan
}
func (b *Boomer) GetTestCaseBytes() []byte {
switch b.mode {
case DistributedMasterMode:
return b.masterRunner.testCasesBytes
case DistributedWorkerMode:
return b.workerRunner.testCasesBytes
default:
return nil
}
}
func ProfileToBytes(profile *Profile) []byte {
profileBytes, err := json.Marshal(profile)
if err != nil {
log.Error().Err(err).Msg("failed to marshal testcases")
return nil
}
return profileBytes
}
func BytesToProfile(profileBytes []byte) *Profile {
var profile *Profile
err := json.Unmarshal(profileBytes, &profile)
if err != nil {
log.Error().Err(err).Msg("failed to unmarshal testcases")
}
return profile
}
// GetTasksChan getsTasks chan
func (b *Boomer) GetTasksChan() chan *task {
switch b.mode {
case DistributedWorkerMode:
return b.workerRunner.tasksChan
default:
return nil
}
}
func (b *Boomer) GetRebalanceChan() chan bool {
switch b.mode {
case DistributedWorkerMode:
return b.workerRunner.controller.getRebalanceChan()
default:
return nil
}
}
func (b *Boomer) SetTestCasesPath(paths []string) {
b.testcasePath = paths
}
func (b *Boomer) GetTestCasesPath() []string {
return b.testcasePath
}
func (b *Boomer) ParseTestCasesChan() chan bool {
return b.masterRunner.parseTestCasesChan
}
// GetMasterHost returns master IP
func (b *Boomer) GetMasterHost() string {
return b.masterHost
}
// GetState gets worker state
func (b *Boomer) GetState() int32 {
switch b.mode {
case DistributedWorkerMode:
return b.workerRunner.getState()
case DistributedMasterMode:
return b.masterRunner.getState()
default:
return b.localRunner.getState()
}
}
// SetSpawnCount sets spawn count
func (b *Boomer) SetSpawnCount(spawnCount int64) {
switch b.mode {
case DistributedMasterMode:
b.masterRunner.setSpawnCount(spawnCount)
case DistributedWorkerMode:
b.workerRunner.setSpawnCount(spawnCount)
default:
b.localRunner.setSpawnCount(spawnCount)
}
}
// SetSpawnRate sets spawn rate
func (b *Boomer) SetSpawnRate(spawnRate float64) {
switch b.mode {
case DistributedMasterMode:
b.masterRunner.setSpawnRate(spawnRate)
case DistributedWorkerMode:
b.workerRunner.setSpawnRate(spawnRate)
default:
b.localRunner.setSpawnRate(spawnRate)
}
}
// SetRunTime sets run time
func (b *Boomer) SetRunTime(runTime int64) {
switch b.mode {
case DistributedMasterMode:
b.masterRunner.setRunTime(runTime)
case DistributedWorkerMode:
b.workerRunner.setRunTime(runTime)
default:
b.localRunner.setRunTime(runTime)
}
}
// SetExpectWorkers sets expect workers while load testing
func (b *Boomer) SetExpectWorkers(expectWorkers int, expectWorkersMaxWait int) {
b.masterRunner.setExpectWorkers(expectWorkers, expectWorkersMaxWait)
}
// SetRateLimiter creates rate limiter with the given limit and burst.
func (b *Boomer) SetRateLimiter(maxRPS int64, requestIncreaseRate string) {
var rateLimiter RateLimiter
var err error
if requestIncreaseRate != "-1" {
if maxRPS <= 0 {
maxRPS = math.MaxInt64
}
log.Warn().Int64("maxRPS", maxRPS).Str("increaseRate", requestIncreaseRate).Msg("set ramp up rate limiter")
rateLimiter, err = NewRampUpRateLimiter(maxRPS, requestIncreaseRate, time.Second)
} else {
if maxRPS > 0 {
log.Warn().Int64("maxRPS", maxRPS).Msg("set stable rate limiter")
rateLimiter = NewStableRateLimiter(maxRPS, time.Second)
}
}
if err != nil {
log.Error().Err(err).Msg("failed to create rate limiter")
return
}
if rateLimiter != nil {
switch b.mode {
case DistributedWorkerMode:
b.workerRunner.rateLimitEnabled = true
b.workerRunner.rateLimiter = rateLimiter
case StandaloneMode:
b.localRunner.rateLimitEnabled = true
b.localRunner.rateLimiter = rateLimiter
}
}
}
// SetDisableKeepAlive disable keep-alive for tcp
func (b *Boomer) SetDisableKeepAlive(disableKeepalive bool) {
b.disableKeepalive = disableKeepalive
}
// SetIgnoreQuit not quit while master quit
func (b *Boomer) SetIgnoreQuit() {
b.workerRunner.ignoreQuit = true
}
// SetDisableCompression disable compression to prevent the Transport from requesting compression with an "Accept-Encoding: gzip"
func (b *Boomer) SetDisableCompression(disableCompression bool) {
b.disableCompression = disableCompression
}
func (b *Boomer) GetDisableKeepAlive() bool {
return b.disableKeepalive
}
func (b *Boomer) GetDisableCompression() bool {
return b.disableCompression
}
// SetLoopCount set loop count for test.
func (b *Boomer) SetLoopCount(loopCount int64) {
// total loop count for testcase, it will be evenly distributed to each worker
switch b.mode {
case DistributedWorkerMode:
b.workerRunner.loop = &Loop{loopCount: loopCount * b.workerRunner.getSpawnCount()}
case DistributedMasterMode:
b.masterRunner.loop = &Loop{loopCount: loopCount * b.masterRunner.getSpawnCount()}
case StandaloneMode:
b.localRunner.loop = &Loop{loopCount: loopCount * b.localRunner.getSpawnCount()}
}
}
// AddOutput accepts outputs which implements the boomer.Output interface.
func (b *Boomer) AddOutput(o Output) {
switch b.mode {
case DistributedWorkerMode:
b.workerRunner.addOutput(o)
case DistributedMasterMode:
b.masterRunner.addOutput(o)
case StandaloneMode:
b.localRunner.addOutput(o)
}
}
// EnableCPUProfile will start cpu profiling after run.
func (b *Boomer) EnableCPUProfile(cpuProfile string, duration time.Duration) {
b.cpuProfile = cpuProfile
b.cpuProfileDuration = duration
}
// EnableMemoryProfile will start memory profiling after run.
func (b *Boomer) EnableMemoryProfile(memoryProfile string, duration time.Duration) {
b.memoryProfile = memoryProfile
b.memoryProfileDuration = duration
}
// EnableGracefulQuit catch SIGINT and SIGTERM signals to quit gracefully
func (b *Boomer) EnableGracefulQuit(ctx context.Context) context.Context {
ctx, cancel := context.WithCancel(ctx)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-c
b.Quit()
cancel()
}()
return ctx
}
// Run accepts a slice of Task and connects to the locust master.
func (b *Boomer) Run(tasks ...*Task) {
if b.cpuProfile != "" {
err := startCPUProfile(b.cpuProfile, b.cpuProfileDuration)
if err != nil {
log.Error().Err(err).Msg("failed to start cpu profiling")
}
}
if b.memoryProfile != "" {
err := startMemoryProfile(b.memoryProfile, b.memoryProfileDuration)
if err != nil {
log.Error().Err(err).Msg("failed to start memory profiling")
}
}
switch b.mode {
case DistributedWorkerMode:
log.Info().Msg("running in worker mode")
b.workerRunner.setTasks(tasks)
b.workerRunner.start()
case StandaloneMode:
log.Info().Msg("running in standalone mode")
b.localRunner.setTasks(tasks)
b.localRunner.start()
default:
log.Error().Err(errors.New("Invalid mode, expected boomer.DistributedMode or boomer.StandaloneMode"))
}
}
func (b *Boomer) SetTasks(tasks ...*Task) {
switch b.mode {
case DistributedWorkerMode:
log.Info().Msg("set tasks to worker")
b.workerRunner.setTasks(tasks)
case StandaloneMode:
log.Info().Msg("set tasks to standalone")
b.localRunner.setTasks(tasks)
default:
log.Error().Err(errors.New("Invalid mode, expected boomer.DistributedMode or boomer.StandaloneMode"))
}
}
// RecordTransaction reports a transaction stat.
func (b *Boomer) RecordTransaction(name string, success bool, elapsedTime int64, contentSize int64) {
var runnerStats *requestStats
switch b.mode {
case DistributedWorkerMode:
runnerStats = b.workerRunner.stats
case DistributedMasterMode:
runnerStats = b.masterRunner.stats
case StandaloneMode:
runnerStats = b.localRunner.stats
}
runnerStats.transactionChan <- &transaction{
name: name,
success: success,
elapsedTime: elapsedTime,
contentSize: contentSize,
}
}
// RecordSuccess reports a success.
func (b *Boomer) RecordSuccess(requestType, name string, responseTime int64, responseLength int64) {
var runnerStats *requestStats
switch b.mode {
case DistributedWorkerMode:
runnerStats = b.workerRunner.stats
case DistributedMasterMode:
runnerStats = b.masterRunner.stats
case StandaloneMode:
runnerStats = b.localRunner.stats
}
runnerStats.requestSuccessChan <- &requestSuccess{
requestType: requestType,
name: name,
responseTime: responseTime,
responseLength: responseLength,
}
}
// RecordFailure reports a failure.
func (b *Boomer) RecordFailure(requestType, name string, responseTime int64, exception string) {
var runnerStats *requestStats
switch b.mode {
case DistributedWorkerMode:
runnerStats = b.workerRunner.stats
case DistributedMasterMode:
runnerStats = b.masterRunner.stats
case StandaloneMode:
runnerStats = b.localRunner.stats
}
runnerStats.requestFailureChan <- &requestFailure{
requestType: requestType,
name: name,
responseTime: responseTime,
errMsg: exception,
}
}
// Start starts to run
func (b *Boomer) Start(Args *Profile) error {
if b.masterRunner.isStarting() {
return errors.New("already started")
}
if b.masterRunner.isStopping() {
return errors.New("Please wait for all workers to finish")
}
if int(Args.SpawnCount) < b.masterRunner.server.getAvailableClientsLength() {
return errors.New("spawn count should be greater than available worker count")
}
b.SetSpawnCount(Args.SpawnCount)
b.SetSpawnRate(Args.SpawnRate)
b.SetRunTime(Args.RunTime)
b.SetProfile(Args)
err := b.masterRunner.start()
return err
}
// ReBalance starts to rebalance load test
func (b *Boomer) ReBalance(Args *Profile) error {
if !b.masterRunner.isStarting() {
return errors.New("no start")
}
if int(Args.SpawnCount) < b.masterRunner.server.getAvailableClientsLength() {
return errors.New("spawn count should be greater than available worker count")
}
b.SetSpawnCount(Args.SpawnCount)
b.SetSpawnRate(Args.SpawnRate)
b.SetRunTime(Args.RunTime)
b.SetProfile(Args)
err := b.masterRunner.rebalance()
if err != nil {
log.Error().Err(err).Msg("failed to rebalance")
}
return err
}
// Stop stops to load test
func (b *Boomer) Stop() error {
return b.masterRunner.stop()
}
// GetWorkersInfo gets workers information
func (b *Boomer) GetWorkersInfo() []WorkerNode {
return b.masterRunner.server.getAllWorkers()
}
// GetMasterInfo gets master information
func (b *Boomer) GetMasterInfo() map[string]interface{} {
masterInfo := make(map[string]interface{})
masterInfo["state"] = b.masterRunner.getState()
masterInfo["workers"] = b.masterRunner.server.getAvailableClientsLength()
masterInfo["target_users"] = b.masterRunner.getSpawnCount()
masterInfo["current_users"] = b.masterRunner.server.getCurrentUsers()
return masterInfo
}
func (b *Boomer) GetCloseChan() chan bool {
switch b.mode {
case DistributedWorkerMode:
return b.workerRunner.closeChan
case DistributedMasterMode:
return b.masterRunner.closeChan
default:
return b.localRunner.closeChan
}
}
// Quit will send a quit message to the master.
func (b *Boomer) Quit() {
switch b.mode {
case DistributedWorkerMode:
b.workerRunner.stop()
b.workerRunner.close()
case DistributedMasterMode:
b.masterRunner.close()
case StandaloneMode:
b.localRunner.stop()
}
}
func (b *Boomer) GetSpawnDoneChan() chan struct{} {
switch b.mode {
case DistributedWorkerMode:
return b.workerRunner.controller.getSpawnDone()
case DistributedMasterMode:
return b.masterRunner.controller.getSpawnDone()
default:
return b.localRunner.controller.getSpawnDone()
}
}
func (b *Boomer) GetSpawnCount() int {
switch b.mode {
case DistributedWorkerMode:
return int(b.workerRunner.getSpawnCount())
case DistributedMasterMode:
return int(b.masterRunner.getSpawnCount())
default:
return int(b.localRunner.getSpawnCount())
}
}
func (b *Boomer) ResetStartTime() {
switch b.mode {
case DistributedWorkerMode:
b.workerRunner.stats.total.resetStartTime()
case DistributedMasterMode:
b.masterRunner.stats.total.resetStartTime()
default:
b.localRunner.stats.total.resetStartTime()
}
}