mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-26 10:50:12 +08:00
refactor: move boomer from hrp internal to pkg
This commit is contained in:
5
hrp/pkg/boomer/README.md
Normal file
5
hrp/pkg/boomer/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# boomer
|
||||
|
||||
This module is initially forked from [myzhan/boomer] and made a lot of changes.
|
||||
|
||||
[myzhan/boomer]: https://github.com/myzhan/boomer
|
||||
613
hrp/pkg/boomer/boomer.go
Normal file
613
hrp/pkg/boomer/boomer.go
Normal file
@@ -0,0 +1,613 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
146
hrp/pkg/boomer/boomer_test.go
Normal file
146
hrp/pkg/boomer/boomer_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewStandaloneBoomer(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
|
||||
if b.localRunner.spawnCount != 100 {
|
||||
t.Error("spawnCount should be 100")
|
||||
}
|
||||
|
||||
if b.localRunner.spawnRate != 10 {
|
||||
t.Error("spawnRate should be 10")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRateLimiter(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.SetRateLimiter(10, "10/1s")
|
||||
|
||||
if b.localRunner.rateLimiter == nil {
|
||||
t.Error("b.rateLimiter should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddOutput(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.AddOutput(NewConsoleOutput())
|
||||
b.AddOutput(NewConsoleOutput())
|
||||
|
||||
if len(b.localRunner.outputs) != 2 {
|
||||
t.Error("length of outputs should be 2")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableCPUProfile(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.EnableCPUProfile("cpu.prof", time.Second)
|
||||
|
||||
if b.cpuProfile != "cpu.prof" {
|
||||
t.Error("cpuProfile should be cpu.prof")
|
||||
}
|
||||
|
||||
if b.cpuProfileDuration != time.Second {
|
||||
t.Error("cpuProfileDuration should 1 second")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableMemoryProfile(t *testing.T) {
|
||||
b := NewStandaloneBoomer(100, 10)
|
||||
b.EnableMemoryProfile("mem.prof", time.Second)
|
||||
|
||||
if b.memoryProfile != "mem.prof" {
|
||||
t.Error("memoryProfile should be mem.prof")
|
||||
}
|
||||
|
||||
if b.memoryProfileDuration != time.Second {
|
||||
t.Error("memoryProfileDuration should 1 second")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStandaloneRun(t *testing.T) {
|
||||
b := NewStandaloneBoomer(10, 10)
|
||||
b.EnableCPUProfile("cpu.pprof", 2*time.Second)
|
||||
b.EnableMemoryProfile("mem.pprof", 2*time.Second)
|
||||
|
||||
count := int64(0)
|
||||
taskA := &Task{
|
||||
Name: "increaseCount",
|
||||
Fn: func() {
|
||||
atomic.AddInt64(&count, 1)
|
||||
runtime.Goexit()
|
||||
},
|
||||
}
|
||||
go b.Run(taskA)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
b.Quit()
|
||||
|
||||
if atomic.LoadInt64(&count) != 10 {
|
||||
t.Error("count is", count, "expected: 10")
|
||||
}
|
||||
|
||||
if _, err := os.Stat("cpu.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File cpu.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("cpu.pprof")
|
||||
}
|
||||
|
||||
if _, err := os.Stat("mem.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File mem.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("mem.pprof")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateRatelimiter(t *testing.T) {
|
||||
b := NewStandaloneBoomer(10, 10)
|
||||
b.SetRateLimiter(100, "-1")
|
||||
|
||||
if stableRateLimiter, ok := b.localRunner.rateLimiter.(*StableRateLimiter); !ok {
|
||||
t.Error("Expected stableRateLimiter")
|
||||
} else {
|
||||
if stableRateLimiter.threshold != 100 {
|
||||
t.Error("threshold should be equals to math.MaxInt64, was", stableRateLimiter.threshold)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetRateLimiter(0, "1")
|
||||
if rampUpRateLimiter, ok := b.localRunner.rateLimiter.(*RampUpRateLimiter); !ok {
|
||||
t.Error("Expected rampUpRateLimiter")
|
||||
} else {
|
||||
if rampUpRateLimiter.maxThreshold != math.MaxInt64 {
|
||||
t.Error("maxThreshold should be equals to math.MaxInt64, was", rampUpRateLimiter.maxThreshold)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpRate != "1" {
|
||||
t.Error("rampUpRate should be equals to \"1\", was", rampUpRateLimiter.rampUpRate)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetRateLimiter(10, "2/2s")
|
||||
if rampUpRateLimiter, ok := b.localRunner.rateLimiter.(*RampUpRateLimiter); !ok {
|
||||
t.Error("Expected rampUpRateLimiter")
|
||||
} else {
|
||||
if rampUpRateLimiter.maxThreshold != 10 {
|
||||
t.Error("maxThreshold should be equals to 10, was", rampUpRateLimiter.maxThreshold)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpRate != "2/2s" {
|
||||
t.Error("rampUpRate should be equals to \"2/2s\", was", rampUpRateLimiter.rampUpRate)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpStep != 2 {
|
||||
t.Error("rampUpStep should be equals to 2, was", rampUpRateLimiter.rampUpStep)
|
||||
}
|
||||
if rampUpRateLimiter.rampUpPeroid != 2*time.Second {
|
||||
t.Error("rampUpPeroid should be equals to 2 seconds, was", rampUpRateLimiter.rampUpPeroid)
|
||||
}
|
||||
}
|
||||
}
|
||||
336
hrp/pkg/boomer/client_grpc.go
Normal file
336
hrp/pkg/boomer/client_grpc.go
Normal file
@@ -0,0 +1,336 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/oauth"
|
||||
"google.golang.org/grpc/metadata"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer/data"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager"
|
||||
)
|
||||
|
||||
type grpcClient struct {
|
||||
messager.MessageClient
|
||||
masterHost string
|
||||
masterPort int
|
||||
identity string // nodeID
|
||||
|
||||
config *grpcClientConfig
|
||||
|
||||
fromMaster chan *genericMessage
|
||||
toMaster chan *genericMessage
|
||||
disconnectedChan chan bool
|
||||
shutdownChan chan bool
|
||||
|
||||
failCount int32
|
||||
}
|
||||
|
||||
type grpcClientConfig struct {
|
||||
// ctx is used for the lifetime of the stream that may need to be canceled
|
||||
// on client shutdown.
|
||||
ctx context.Context
|
||||
ctxCancel context.CancelFunc
|
||||
conn *grpc.ClientConn
|
||||
biStream messager.Message_BidirectionalStreamingMessageClient
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
const token = "httprunner-secret-token"
|
||||
|
||||
// unaryInterceptor is an example unary interceptor.
|
||||
func unaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
var credsConfigured bool
|
||||
for _, o := range opts {
|
||||
_, ok := o.(grpc.PerRPCCredsCallOption)
|
||||
if ok {
|
||||
credsConfigured = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !credsConfigured {
|
||||
opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
|
||||
AccessToken: token,
|
||||
})))
|
||||
}
|
||||
start := time.Now()
|
||||
err := invoker(ctx, method, req, reply, cc, opts...)
|
||||
end := time.Now()
|
||||
logger("RPC: %s, start time: %s, end time: %s, err: %v", method, start.Format("Basic"), end.Format(time.RFC3339), err)
|
||||
return err
|
||||
}
|
||||
|
||||
// wrappedStream wraps around the embedded grpc.ClientStream, and intercepts the RecvMsg and
|
||||
// SendMsg method call.
|
||||
type wrappedStream struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (w *wrappedStream) RecvMsg(m interface{}) error {
|
||||
logger("Receive a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
|
||||
return w.ClientStream.RecvMsg(m)
|
||||
}
|
||||
|
||||
func (w *wrappedStream) SendMsg(m interface{}) error {
|
||||
logger("Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
|
||||
return w.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func newWrappedStream(s grpc.ClientStream) grpc.ClientStream {
|
||||
return &wrappedStream{s}
|
||||
}
|
||||
|
||||
func extractToken(ctx context.Context) (tkn string, ok bool) {
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok || len(md[token]) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return md[token][0], true
|
||||
}
|
||||
|
||||
// streamInterceptor is an example stream interceptor.
|
||||
func streamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
var credsConfigured bool
|
||||
for _, o := range opts {
|
||||
_, ok := o.(*grpc.PerRPCCredsCallOption)
|
||||
if ok {
|
||||
credsConfigured = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !credsConfigured {
|
||||
opts = append(opts, grpc.PerRPCCredentials(oauth.NewOauthAccess(&oauth2.Token{
|
||||
AccessToken: token,
|
||||
})))
|
||||
}
|
||||
s, err := streamer(ctx, desc, cc, method, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWrappedStream(s), nil
|
||||
}
|
||||
|
||||
func (c *grpcClientConfig) getBiStreamClient() messager.Message_BidirectionalStreamingMessageClient {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
return c.biStream
|
||||
}
|
||||
|
||||
func (c *grpcClientConfig) setBiStreamClient(s messager.Message_BidirectionalStreamingMessageClient) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.biStream = s
|
||||
}
|
||||
|
||||
func newClient(masterHost string, masterPort int, identity string) (client *grpcClient) {
|
||||
log.Info().Msg("Boomer is built with grpc support.")
|
||||
// Initiate the stream with a context that supports cancellation.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
client = &grpcClient{
|
||||
masterHost: masterHost,
|
||||
masterPort: masterPort,
|
||||
identity: identity,
|
||||
fromMaster: make(chan *genericMessage, 100),
|
||||
toMaster: make(chan *genericMessage, 100),
|
||||
disconnectedChan: make(chan bool),
|
||||
shutdownChan: make(chan bool),
|
||||
config: &grpcClientConfig{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
mutex: sync.RWMutex{},
|
||||
},
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *grpcClient) start() (err error) {
|
||||
addr := fmt.Sprintf("%v:%v", c.masterHost, c.masterPort)
|
||||
// Create tls based credential.
|
||||
creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca_cert.pem"), "www.httprunner.com")
|
||||
if err != nil {
|
||||
log.Fatal().Msg(fmt.Sprintf("failed to load credentials: %v", err))
|
||||
}
|
||||
opts := []grpc.DialOption{
|
||||
// oauth.NewOauthAccess requires the configuration of transport
|
||||
// credentials.
|
||||
grpc.WithTransportCredentials(creds),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(32 * 10e9)),
|
||||
grpc.WithUnaryInterceptor(unaryInterceptor),
|
||||
grpc.WithStreamInterceptor(streamInterceptor),
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: backoff.Config{
|
||||
BaseDelay: 1 * time.Second,
|
||||
Multiplier: 1.2,
|
||||
MaxDelay: 3 * time.Second,
|
||||
},
|
||||
MinConnectTimeout: 3 * time.Second,
|
||||
}),
|
||||
}
|
||||
c.config.conn, err = grpc.Dial(addr, opts...)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to connect")
|
||||
return err
|
||||
}
|
||||
c.MessageClient = messager.NewMessageClient(c.config.conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *grpcClient) register(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
res, err := c.Register(ctx, &messager.RegisterRequest{NodeID: c.identity, Os: runtime.GOOS, Arch: runtime.GOARCH})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Code != "0" {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *grpcClient) signOut(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := c.SignOut(ctx, &messager.SignOutRequest{NodeID: c.identity})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Code != "0" {
|
||||
return errors.New(res.Message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *grpcClient) newBiStreamClient() (err error) {
|
||||
md := metadata.New(map[string]string{token: c.identity})
|
||||
ctx := metadata.NewOutgoingContext(c.config.ctx, md)
|
||||
biStream, err := c.BidirectionalStreamingMessage(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// reset failCount
|
||||
atomic.StoreInt32(&c.failCount, 0)
|
||||
// set bidirectional stream client
|
||||
c.config.setBiStreamClient(biStream)
|
||||
println("successful to establish bidirectional stream with master, press Ctrl+c to quit.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *grpcClient) recvChannel() chan *genericMessage {
|
||||
return c.fromMaster
|
||||
}
|
||||
|
||||
func (c *grpcClient) recv() {
|
||||
for {
|
||||
select {
|
||||
case <-c.shutdownChan:
|
||||
return
|
||||
default:
|
||||
if c.config.getBiStreamClient() == nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
msg, err := c.config.getBiStreamClient().Recv()
|
||||
if err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
// log.Error().Err(err).Msg("failed to get message")
|
||||
continue
|
||||
}
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if msg.NodeID != c.identity {
|
||||
log.Info().
|
||||
Str("nodeID", msg.NodeID).
|
||||
Str("type", msg.Type).
|
||||
Interface("data", msg.Data).
|
||||
Msg(fmt.Sprintf("not for me(%s)", c.identity))
|
||||
continue
|
||||
}
|
||||
|
||||
c.fromMaster <- &genericMessage{
|
||||
Type: msg.Type,
|
||||
Profile: msg.Profile,
|
||||
Data: msg.Data,
|
||||
NodeID: msg.NodeID,
|
||||
Tasks: msg.Tasks,
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("nodeID", msg.NodeID).
|
||||
Str("type", msg.Type).
|
||||
Interface("data", msg.Data).
|
||||
Interface("tasks", msg.Tasks).
|
||||
Msg("receive data from master")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *grpcClient) sendChannel() chan *genericMessage {
|
||||
return c.toMaster
|
||||
}
|
||||
|
||||
func (c *grpcClient) send() {
|
||||
for {
|
||||
select {
|
||||
case <-c.shutdownChan:
|
||||
return
|
||||
case msg := <-c.toMaster:
|
||||
c.sendMessage(msg)
|
||||
|
||||
// We may send genericMessage to master.
|
||||
switch msg.Type {
|
||||
case "quit":
|
||||
c.disconnectedChan <- true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *grpcClient) sendMessage(msg *genericMessage) {
|
||||
log.Info().
|
||||
Str("nodeID", msg.NodeID).
|
||||
Str("type", msg.Type).
|
||||
Interface("data", msg.Data).
|
||||
Msg("send data to server")
|
||||
if c.config.getBiStreamClient() == nil {
|
||||
atomic.AddInt32(&c.failCount, 1)
|
||||
return
|
||||
}
|
||||
err := c.config.getBiStreamClient().Send(&messager.StreamRequest{Type: msg.Type, Data: msg.Data, NodeID: msg.NodeID})
|
||||
if err == nil {
|
||||
atomic.StoreInt32(&c.failCount, 0)
|
||||
return
|
||||
}
|
||||
// log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message")
|
||||
if msg.Type == "heartbeat" {
|
||||
atomic.AddInt32(&c.failCount, 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *grpcClient) disconnectedChannel() chan bool {
|
||||
return c.disconnectedChan
|
||||
}
|
||||
|
||||
func (c *grpcClient) close() {
|
||||
close(c.shutdownChan)
|
||||
c.config.ctxCancel()
|
||||
if c.config.conn != nil {
|
||||
c.config.conn.Close()
|
||||
}
|
||||
}
|
||||
1
hrp/pkg/boomer/client_grpc_test.go
Normal file
1
hrp/pkg/boomer/client_grpc_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package boomer
|
||||
61
hrp/pkg/boomer/data/data.go
Normal file
61
hrp/pkg/boomer/data/data.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
)
|
||||
|
||||
// hrpPath is .hrp directory under the user directory.
|
||||
var hrpPath string
|
||||
|
||||
//go:embed x509/*
|
||||
var x509Dir embed.FS
|
||||
|
||||
func init() {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hrpPath = filepath.Join(home, ".hrp")
|
||||
_ = builtin.EnsureFolderExists(filepath.Join(hrpPath, "x509"))
|
||||
}
|
||||
|
||||
// Path returns the absolute path the given relative file or directory path
|
||||
func Path(rel string) (destPath string) {
|
||||
destPath = rel
|
||||
if !filepath.IsAbs(rel) {
|
||||
destPath = filepath.Join(hrpPath, rel)
|
||||
}
|
||||
if !builtin.IsFilePathExists(destPath) {
|
||||
content, err := x509Dir.ReadFile(rel)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = os.WriteFile(destPath, content, 0o644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
6
hrp/pkg/boomer/data/x509/README.md
Normal file
6
hrp/pkg/boomer/data/x509/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
This directory contains x509 certificates and associated private keys used in
|
||||
examples.
|
||||
|
||||
How were these test certs/keys generated ?
|
||||
------------------------------------------
|
||||
Run `./create.sh`
|
||||
34
hrp/pkg/boomer/data/x509/ca_cert.pem
Normal file
34
hrp/pkg/boomer/data/x509/ca_cert.pem
Normal file
@@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
52
hrp/pkg/boomer/data/x509/ca_key.pem
Normal file
52
hrp/pkg/boomer/data/x509/ca_key.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
8xnynnp57i3BHHHbjY4R02Mqsfi1nNoN
|
||||
-----END PRIVATE KEY-----
|
||||
34
hrp/pkg/boomer/data/x509/client_ca_cert.pem
Normal file
34
hrp/pkg/boomer/data/x509/client_ca_cert.pem
Normal file
@@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
52
hrp/pkg/boomer/data/x509/client_ca_key.pem
Normal file
52
hrp/pkg/boomer/data/x509/client_ca_key.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
+3KzjPLML31OD8TN22FUzsYcdw2prEU=
|
||||
-----END PRIVATE KEY-----
|
||||
32
hrp/pkg/boomer/data/x509/client_cert.pem
Normal file
32
hrp/pkg/boomer/data/x509/client_cert.pem
Normal file
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
MtkfvRA=
|
||||
-----END CERTIFICATE-----
|
||||
51
hrp/pkg/boomer/data/x509/client_key.pem
Normal file
51
hrp/pkg/boomer/data/x509/client_key.pem
Normal file
@@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
69
hrp/pkg/boomer/data/x509/create.sh
Executable file
69
hrp/pkg/boomer/data/x509/create.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create the server CA certs.
|
||||
openssl req -x509 \
|
||||
-newkey rsa:4096 \
|
||||
-nodes \
|
||||
-days 3650 \
|
||||
-keyout ca_key.pem \
|
||||
-out ca_cert.pem \
|
||||
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-server_ca/ \
|
||||
-config ./openssl.cnf \
|
||||
-extensions test_ca \
|
||||
-sha256
|
||||
|
||||
# Create the client CA certs.
|
||||
openssl req -x509 \
|
||||
-newkey rsa:4096 \
|
||||
-nodes \
|
||||
-days 3650 \
|
||||
-keyout client_ca_key.pem \
|
||||
-out client_ca_cert.pem \
|
||||
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client_ca/ \
|
||||
-config ./openssl.cnf \
|
||||
-extensions test_ca \
|
||||
-sha256
|
||||
|
||||
# Generate a server cert.
|
||||
openssl genrsa -out server_key.pem 4096
|
||||
openssl req -new \
|
||||
-key server_key.pem \
|
||||
-days 3650 \
|
||||
-out server_csr.pem \
|
||||
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-server1/ \
|
||||
-config ./openssl.cnf \
|
||||
-reqexts test_server
|
||||
openssl x509 -req \
|
||||
-in server_csr.pem \
|
||||
-CAkey ca_key.pem \
|
||||
-CA ca_cert.pem \
|
||||
-days 3650 \
|
||||
-set_serial 1000 \
|
||||
-out server_cert.pem \
|
||||
-extfile ./openssl.cnf \
|
||||
-extensions test_server \
|
||||
-sha256
|
||||
openssl verify -verbose -CAfile ca_cert.pem server_cert.pem
|
||||
|
||||
# Generate a client cert.
|
||||
openssl genrsa -out client_key.pem 4096
|
||||
openssl req -new \
|
||||
-key client_key.pem \
|
||||
-days 3650 \
|
||||
-out client_csr.pem \
|
||||
-subj /C=US/ST=CA/L=SVL/O=gRPC/CN=test-client1/ \
|
||||
-config ./openssl.cnf \
|
||||
-reqexts test_client
|
||||
openssl x509 -req \
|
||||
-in client_csr.pem \
|
||||
-CAkey client_ca_key.pem \
|
||||
-CA client_ca_cert.pem \
|
||||
-days 3650 \
|
||||
-set_serial 1000 \
|
||||
-out client_cert.pem \
|
||||
-extfile ./openssl.cnf \
|
||||
-extensions test_client \
|
||||
-sha256
|
||||
openssl verify -verbose -CAfile client_ca_cert.pem client_cert.pem
|
||||
|
||||
rm *_csr.pem
|
||||
28
hrp/pkg/boomer/data/x509/openssl.cnf
Normal file
28
hrp/pkg/boomer/data/x509/openssl.cnf
Normal file
@@ -0,0 +1,28 @@
|
||||
[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
attributes = req_attributes
|
||||
|
||||
[req_distinguished_name]
|
||||
|
||||
[req_attributes]
|
||||
|
||||
[test_ca]
|
||||
basicConstraints = critical,CA:TRUE
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer:always
|
||||
keyUsage = critical,keyCertSign
|
||||
|
||||
[test_server]
|
||||
basicConstraints = critical,CA:FALSE
|
||||
subjectKeyIdentifier = hash
|
||||
keyUsage = critical,digitalSignature,keyEncipherment,keyAgreement
|
||||
subjectAltName = @server_alt_names
|
||||
|
||||
[server_alt_names]
|
||||
DNS.1 = *.httprunner.com
|
||||
|
||||
[test_client]
|
||||
basicConstraints = critical,CA:FALSE
|
||||
subjectKeyIdentifier = hash
|
||||
keyUsage = critical,nonRepudiation,digitalSignature,keyEncipherment
|
||||
extendedKeyUsage = critical,clientAuth
|
||||
32
hrp/pkg/boomer/data/x509/server_cert.pem
Normal file
32
hrp/pkg/boomer/data/x509/server_cert.pem
Normal file
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
TxGOtJdjo0IEAQ==
|
||||
-----END CERTIFICATE-----
|
||||
51
hrp/pkg/boomer/data/x509/server_key.pem
Normal file
51
hrp/pkg/boomer/data/x509/server_key.pem
Normal file
@@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----
|
||||
589
hrp/pkg/boomer/grpc/messager/messager.pb.go
Normal file
589
hrp/pkg/boomer/grpc/messager/messager.pb.go
Normal file
@@ -0,0 +1,589 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.0
|
||||
// protoc v3.19.4
|
||||
// source: grpc/proto/messager.proto
|
||||
|
||||
package messager
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type StreamRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
NodeID string `protobuf:"bytes,2,opt,name=nodeID,proto3" json:"nodeID,omitempty"`
|
||||
Data map[string][]byte `protobuf:"bytes,3,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *StreamRequest) Reset() {
|
||||
*x = StreamRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *StreamRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StreamRequest) ProtoMessage() {}
|
||||
|
||||
func (x *StreamRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StreamRequest.ProtoReflect.Descriptor instead.
|
||||
func (*StreamRequest) Descriptor() ([]byte, []int) {
|
||||
return file_grpc_proto_messager_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *StreamRequest) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StreamRequest) GetNodeID() string {
|
||||
if x != nil {
|
||||
return x.NodeID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StreamRequest) GetData() map[string][]byte {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type StreamResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"`
|
||||
NodeID string `protobuf:"bytes,2,opt,name=nodeID,proto3" json:"nodeID,omitempty"`
|
||||
Profile []byte `protobuf:"bytes,3,opt,name=profile,proto3" json:"profile,omitempty"`
|
||||
Tasks []byte `protobuf:"bytes,4,opt,name=tasks,proto3" json:"tasks,omitempty"`
|
||||
Data map[string][]byte `protobuf:"bytes,5,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *StreamResponse) Reset() {
|
||||
*x = StreamResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *StreamResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*StreamResponse) ProtoMessage() {}
|
||||
|
||||
func (x *StreamResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use StreamResponse.ProtoReflect.Descriptor instead.
|
||||
func (*StreamResponse) Descriptor() ([]byte, []int) {
|
||||
return file_grpc_proto_messager_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *StreamResponse) GetType() string {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StreamResponse) GetNodeID() string {
|
||||
if x != nil {
|
||||
return x.NodeID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *StreamResponse) GetProfile() []byte {
|
||||
if x != nil {
|
||||
return x.Profile
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamResponse) GetTasks() []byte {
|
||||
if x != nil {
|
||||
return x.Tasks
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StreamResponse) GetData() map[string][]byte {
|
||||
if x != nil {
|
||||
return x.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
NodeID string `protobuf:"bytes,1,opt,name=nodeID,proto3" json:"nodeID,omitempty"`
|
||||
Os string `protobuf:"bytes,2,opt,name=os,proto3" json:"os,omitempty"`
|
||||
Arch string `protobuf:"bytes,3,opt,name=arch,proto3" json:"arch,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RegisterRequest) Reset() {
|
||||
*x = RegisterRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RegisterRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RegisterRequest) ProtoMessage() {}
|
||||
|
||||
func (x *RegisterRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RegisterRequest.ProtoReflect.Descriptor instead.
|
||||
func (*RegisterRequest) Descriptor() ([]byte, []int) {
|
||||
return file_grpc_proto_messager_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *RegisterRequest) GetNodeID() string {
|
||||
if x != nil {
|
||||
return x.NodeID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterRequest) GetOs() string {
|
||||
if x != nil {
|
||||
return x.Os
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterRequest) GetArch() string {
|
||||
if x != nil {
|
||||
return x.Arch
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RegisterResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (x *RegisterResponse) Reset() {
|
||||
*x = RegisterResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *RegisterResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RegisterResponse) ProtoMessage() {}
|
||||
|
||||
func (x *RegisterResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RegisterResponse.ProtoReflect.Descriptor instead.
|
||||
func (*RegisterResponse) Descriptor() ([]byte, []int) {
|
||||
return file_grpc_proto_messager_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *RegisterResponse) GetCode() string {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RegisterResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SignOutRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
NodeID string `protobuf:"bytes,1,opt,name=nodeID,proto3" json:"nodeID,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SignOutRequest) Reset() {
|
||||
*x = SignOutRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SignOutRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SignOutRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SignOutRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SignOutRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SignOutRequest) Descriptor() ([]byte, []int) {
|
||||
return file_grpc_proto_messager_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *SignOutRequest) GetNodeID() string {
|
||||
if x != nil {
|
||||
return x.NodeID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SignOutResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Code string `protobuf:"bytes,1,opt,name=code,proto3" json:"code,omitempty"`
|
||||
Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (x *SignOutResponse) Reset() {
|
||||
*x = SignOutResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *SignOutResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SignOutResponse) ProtoMessage() {}
|
||||
|
||||
func (x *SignOutResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_grpc_proto_messager_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SignOutResponse.ProtoReflect.Descriptor instead.
|
||||
func (*SignOutResponse) Descriptor() ([]byte, []int) {
|
||||
return file_grpc_proto_messager_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *SignOutResponse) GetCode() string {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SignOutResponse) GetMessage() string {
|
||||
if x != nil {
|
||||
return x.Message
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_grpc_proto_messager_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_grpc_proto_messager_proto_rawDesc = []byte{
|
||||
0x0a, 0x19, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x65, 0x73,
|
||||
0x73, 0x61, 0x67, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6d, 0x65, 0x73,
|
||||
0x73, 0x61, 0x67, 0x65, 0x22, 0xaa, 0x01, 0x0a, 0x0d, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52,
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f,
|
||||
0x64, 0x65, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65,
|
||||
0x49, 0x44, 0x12, 0x34, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b,
|
||||
0x32, 0x20, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61,
|
||||
0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74,
|
||||
0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61,
|
||||
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
|
||||
0x01, 0x22, 0xdc, 0x01, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70,
|
||||
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x64, 0x65,
|
||||
0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44,
|
||||
0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x0c, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61,
|
||||
0x73, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73,
|
||||
0x12, 0x35, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21,
|
||||
0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52,
|
||||
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72,
|
||||
0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09, 0x44, 0x61, 0x74, 0x61, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18,
|
||||
0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01,
|
||||
0x22, 0x4d, 0x0a, 0x0f, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x12, 0x0e, 0x0a, 0x02, 0x6f,
|
||||
0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x61,
|
||||
0x72, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x63, 0x68, 0x22,
|
||||
0x40, 0x0a, 0x10, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
|
||||
0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||
0x65, 0x22, 0x28, 0x0a, 0x0e, 0x53, 0x69, 0x67, 0x6e, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x44, 0x22, 0x3f, 0x0a, 0x0f, 0x53,
|
||||
0x69, 0x67, 0x6e, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f,
|
||||
0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0xe4, 0x01, 0x0a,
|
||||
0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x41, 0x0a, 0x08, 0x52, 0x65, 0x67, 0x69,
|
||||
0x73, 0x74, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x52,
|
||||
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19,
|
||||
0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
|
||||
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x3e, 0x0a, 0x07, 0x53,
|
||||
0x69, 0x67, 0x6e, 0x4f, 0x75, 0x74, 0x12, 0x17, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||
0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||
0x18, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4f, 0x75,
|
||||
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x1d, 0x42,
|
||||
0x69, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x72, 0x65,
|
||||
0x61, 0x6d, 0x69, 0x6e, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x16, 0x2e, 0x6d,
|
||||
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x53,
|
||||
0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28,
|
||||
0x01, 0x30, 0x01, 0x42, 0x0f, 0x5a, 0x0d, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x6d, 0x65, 0x73, 0x73,
|
||||
0x61, 0x67, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_grpc_proto_messager_proto_rawDescOnce sync.Once
|
||||
file_grpc_proto_messager_proto_rawDescData = file_grpc_proto_messager_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_grpc_proto_messager_proto_rawDescGZIP() []byte {
|
||||
file_grpc_proto_messager_proto_rawDescOnce.Do(func() {
|
||||
file_grpc_proto_messager_proto_rawDescData = protoimpl.X.CompressGZIP(file_grpc_proto_messager_proto_rawDescData)
|
||||
})
|
||||
return file_grpc_proto_messager_proto_rawDescData
|
||||
}
|
||||
|
||||
var (
|
||||
file_grpc_proto_messager_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
file_grpc_proto_messager_proto_goTypes = []interface{}{
|
||||
(*StreamRequest)(nil), // 0: message.StreamRequest
|
||||
(*StreamResponse)(nil), // 1: message.StreamResponse
|
||||
(*RegisterRequest)(nil), // 2: message.RegisterRequest
|
||||
(*RegisterResponse)(nil), // 3: message.RegisterResponse
|
||||
(*SignOutRequest)(nil), // 4: message.SignOutRequest
|
||||
(*SignOutResponse)(nil), // 5: message.SignOutResponse
|
||||
nil, // 6: message.StreamRequest.DataEntry
|
||||
nil, // 7: message.StreamResponse.DataEntry
|
||||
}
|
||||
)
|
||||
|
||||
var file_grpc_proto_messager_proto_depIdxs = []int32{
|
||||
6, // 0: message.StreamRequest.data:type_name -> message.StreamRequest.DataEntry
|
||||
7, // 1: message.StreamResponse.data:type_name -> message.StreamResponse.DataEntry
|
||||
2, // 2: message.Message.Register:input_type -> message.RegisterRequest
|
||||
4, // 3: message.Message.SignOut:input_type -> message.SignOutRequest
|
||||
0, // 4: message.Message.BidirectionalStreamingMessage:input_type -> message.StreamRequest
|
||||
3, // 5: message.Message.Register:output_type -> message.RegisterResponse
|
||||
5, // 6: message.Message.SignOut:output_type -> message.SignOutResponse
|
||||
1, // 7: message.Message.BidirectionalStreamingMessage:output_type -> message.StreamResponse
|
||||
5, // [5:8] is the sub-list for method output_type
|
||||
2, // [2:5] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_grpc_proto_messager_proto_init() }
|
||||
func file_grpc_proto_messager_proto_init() {
|
||||
if File_grpc_proto_messager_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_grpc_proto_messager_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*StreamRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_grpc_proto_messager_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*StreamResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_grpc_proto_messager_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RegisterRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_grpc_proto_messager_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*RegisterResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_grpc_proto_messager_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SignOutRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_grpc_proto_messager_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*SignOutResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_grpc_proto_messager_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_grpc_proto_messager_proto_goTypes,
|
||||
DependencyIndexes: file_grpc_proto_messager_proto_depIdxs,
|
||||
MessageInfos: file_grpc_proto_messager_proto_msgTypes,
|
||||
}.Build()
|
||||
File_grpc_proto_messager_proto = out.File
|
||||
file_grpc_proto_messager_proto_rawDesc = nil
|
||||
file_grpc_proto_messager_proto_goTypes = nil
|
||||
file_grpc_proto_messager_proto_depIdxs = nil
|
||||
}
|
||||
212
hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go
Normal file
212
hrp/pkg/boomer/grpc/messager/messager_grpc.pb.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.19.4
|
||||
// source: grpc/proto/messager.proto
|
||||
|
||||
package messager
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// MessageClient is the client API for Message service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type MessageClient interface {
|
||||
Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error)
|
||||
SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*SignOutResponse, error)
|
||||
BidirectionalStreamingMessage(ctx context.Context, opts ...grpc.CallOption) (Message_BidirectionalStreamingMessageClient, error)
|
||||
}
|
||||
|
||||
type messageClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewMessageClient(cc grpc.ClientConnInterface) MessageClient {
|
||||
return &messageClient{cc}
|
||||
}
|
||||
|
||||
func (c *messageClient) Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) {
|
||||
out := new(RegisterResponse)
|
||||
err := c.cc.Invoke(ctx, "/message.Message/Register", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *messageClient) SignOut(ctx context.Context, in *SignOutRequest, opts ...grpc.CallOption) (*SignOutResponse, error) {
|
||||
out := new(SignOutResponse)
|
||||
err := c.cc.Invoke(ctx, "/message.Message/SignOut", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *messageClient) BidirectionalStreamingMessage(ctx context.Context, opts ...grpc.CallOption) (Message_BidirectionalStreamingMessageClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &Message_ServiceDesc.Streams[0], "/message.Message/BidirectionalStreamingMessage", opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &messageBidirectionalStreamingMessageClient{stream}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
type Message_BidirectionalStreamingMessageClient interface {
|
||||
Send(*StreamRequest) error
|
||||
Recv() (*StreamResponse, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
type messageBidirectionalStreamingMessageClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (x *messageBidirectionalStreamingMessageClient) Send(m *StreamRequest) error {
|
||||
return x.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *messageBidirectionalStreamingMessageClient) Recv() (*StreamResponse, error) {
|
||||
m := new(StreamResponse)
|
||||
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// MessageServer is the server API for Message service.
|
||||
// All implementations must embed UnimplementedMessageServer
|
||||
// for forward compatibility
|
||||
type MessageServer interface {
|
||||
Register(context.Context, *RegisterRequest) (*RegisterResponse, error)
|
||||
SignOut(context.Context, *SignOutRequest) (*SignOutResponse, error)
|
||||
BidirectionalStreamingMessage(Message_BidirectionalStreamingMessageServer) error
|
||||
mustEmbedUnimplementedMessageServer()
|
||||
}
|
||||
|
||||
// UnimplementedMessageServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedMessageServer struct{}
|
||||
|
||||
func (UnimplementedMessageServer) Register(context.Context, *RegisterRequest) (*RegisterResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Register not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedMessageServer) SignOut(context.Context, *SignOutRequest) (*SignOutResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SignOut not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedMessageServer) BidirectionalStreamingMessage(Message_BidirectionalStreamingMessageServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method BidirectionalStreamingMessage not implemented")
|
||||
}
|
||||
func (UnimplementedMessageServer) mustEmbedUnimplementedMessageServer() {}
|
||||
|
||||
// UnsafeMessageServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to MessageServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeMessageServer interface {
|
||||
mustEmbedUnimplementedMessageServer()
|
||||
}
|
||||
|
||||
func RegisterMessageServer(s grpc.ServiceRegistrar, srv MessageServer) {
|
||||
s.RegisterService(&Message_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Message_Register_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RegisterRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MessageServer).Register(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/message.Message/Register",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MessageServer).Register(ctx, req.(*RegisterRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Message_SignOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SignOutRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(MessageServer).SignOut(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/message.Message/SignOut",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(MessageServer).SignOut(ctx, req.(*SignOutRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Message_BidirectionalStreamingMessage_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(MessageServer).BidirectionalStreamingMessage(&messageBidirectionalStreamingMessageServer{stream})
|
||||
}
|
||||
|
||||
type Message_BidirectionalStreamingMessageServer interface {
|
||||
Send(*StreamResponse) error
|
||||
Recv() (*StreamRequest, error)
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
type messageBidirectionalStreamingMessageServer struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (x *messageBidirectionalStreamingMessageServer) Send(m *StreamResponse) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *messageBidirectionalStreamingMessageServer) Recv() (*StreamRequest, error) {
|
||||
m := new(StreamRequest)
|
||||
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Message_ServiceDesc is the grpc.ServiceDesc for Message service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Message_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "message.Message",
|
||||
HandlerType: (*MessageServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Register",
|
||||
Handler: _Message_Register_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SignOut",
|
||||
Handler: _Message_SignOut_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "BidirectionalStreamingMessage",
|
||||
Handler: _Message_BidirectionalStreamingMessage_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "grpc/proto/messager.proto",
|
||||
}
|
||||
45
hrp/pkg/boomer/grpc/proto/messager.proto
Normal file
45
hrp/pkg/boomer/grpc/proto/messager.proto
Normal file
@@ -0,0 +1,45 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package message;
|
||||
|
||||
option go_package = "grpc/messager";
|
||||
|
||||
service Message {
|
||||
rpc Register(RegisterRequest) returns (RegisterResponse) {}
|
||||
rpc SignOut(SignOutRequest) returns (SignOutResponse) {}
|
||||
rpc BidirectionalStreamingMessage(stream StreamRequest) returns (stream StreamResponse){};
|
||||
}
|
||||
|
||||
message StreamRequest{
|
||||
string type = 1;
|
||||
string nodeID = 2;
|
||||
map<string, bytes> data = 3;
|
||||
}
|
||||
|
||||
message StreamResponse{
|
||||
string type = 1;
|
||||
string nodeID = 2;
|
||||
bytes profile = 3;
|
||||
bytes tasks = 4;
|
||||
map<string, bytes> data = 5;
|
||||
}
|
||||
|
||||
message RegisterRequest{
|
||||
string nodeID = 1;
|
||||
string os = 2;
|
||||
string arch = 3;
|
||||
}
|
||||
|
||||
message RegisterResponse{
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
|
||||
message SignOutRequest{
|
||||
string nodeID = 1;
|
||||
}
|
||||
|
||||
message SignOutResponse{
|
||||
string code = 1;
|
||||
string message = 2;
|
||||
}
|
||||
55
hrp/pkg/boomer/message.go
Normal file
55
hrp/pkg/boomer/message.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package boomer
|
||||
|
||||
const (
|
||||
typeClientReady = "client_ready"
|
||||
typeClientStopped = "client_stopped"
|
||||
typeHeartbeat = "heartbeat"
|
||||
typeSpawning = "spawning"
|
||||
typeSpawningComplete = "spawning_complete"
|
||||
typeQuit = "quit"
|
||||
typeException = "exception"
|
||||
)
|
||||
|
||||
type genericMessage struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Profile []byte `json:"profile,omitempty"`
|
||||
Data map[string][]byte `json:"data,omitempty"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
Tasks []byte `json:"tasks,omitempty"`
|
||||
}
|
||||
|
||||
type task struct {
|
||||
Profile *Profile `json:"profile,omitempty"`
|
||||
TestCasesBytes []byte `json:"testcases,omitempty"`
|
||||
}
|
||||
|
||||
func newGenericMessage(t string, data map[string][]byte, nodeID string) (msg *genericMessage) {
|
||||
return &genericMessage{
|
||||
Type: t,
|
||||
Data: data,
|
||||
NodeID: nodeID,
|
||||
}
|
||||
}
|
||||
|
||||
func newQuitMessage(nodeID string) (msg *genericMessage) {
|
||||
return &genericMessage{
|
||||
Type: "quit",
|
||||
NodeID: nodeID,
|
||||
}
|
||||
}
|
||||
|
||||
func newMessageToWorker(t string, profile []byte, data map[string][]byte, tasks []byte) (msg *genericMessage) {
|
||||
return &genericMessage{
|
||||
Type: t,
|
||||
Profile: profile,
|
||||
Data: data,
|
||||
Tasks: tasks,
|
||||
}
|
||||
}
|
||||
|
||||
func newClientReadyMessageToMaster(nodeID string) (msg *genericMessage) {
|
||||
return &genericMessage{
|
||||
Type: "client_ready",
|
||||
NodeID: nodeID,
|
||||
}
|
||||
}
|
||||
1
hrp/pkg/boomer/message_test.go
Normal file
1
hrp/pkg/boomer/message_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package boomer
|
||||
646
hrp/pkg/boomer/output.go
Normal file
646
hrp/pkg/boomer/output.go
Normal file
@@ -0,0 +1,646 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/push"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
// Output is primarily responsible for printing test results to different destinations
|
||||
// such as consoles, files. You can write you own output and add to boomer.
|
||||
// When running in standalone mode, the default output is ConsoleOutput, you can add more.
|
||||
// When running in distribute mode, test results will be reported to master with or without
|
||||
// an output.
|
||||
// All the OnXXX function will be call in a separated goroutine, just in case some output will block.
|
||||
// But it will wait for all outputs return to avoid data lost.
|
||||
type Output interface {
|
||||
// OnStart will be call before the test starts.
|
||||
OnStart()
|
||||
|
||||
// By default, each output receive stats data from runner every three seconds.
|
||||
// OnEvent is responsible for dealing with the data.
|
||||
OnEvent(data map[string]interface{})
|
||||
|
||||
// OnStop will be called before the test ends.
|
||||
OnStop()
|
||||
}
|
||||
|
||||
// ConsoleOutput is the default output for standalone mode.
|
||||
type ConsoleOutput struct{}
|
||||
|
||||
// NewConsoleOutput returns a ConsoleOutput.
|
||||
func NewConsoleOutput() *ConsoleOutput {
|
||||
return &ConsoleOutput{}
|
||||
}
|
||||
|
||||
func getMedianResponseTime(numRequests int64, responseTimes map[int64]int64) int64 {
|
||||
medianResponseTime := int64(0)
|
||||
if len(responseTimes) != 0 {
|
||||
pos := (numRequests - 1) / 2
|
||||
var sortedKeys []int64
|
||||
for k := range responseTimes {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Slice(sortedKeys, func(i, j int) bool {
|
||||
return sortedKeys[i] < sortedKeys[j]
|
||||
})
|
||||
for _, k := range sortedKeys {
|
||||
if pos < responseTimes[k] {
|
||||
medianResponseTime = k
|
||||
break
|
||||
}
|
||||
pos -= responseTimes[k]
|
||||
}
|
||||
}
|
||||
return medianResponseTime
|
||||
}
|
||||
|
||||
func getAvgResponseTime(numRequests int64, totalResponseTime int64) (avgResponseTime float64) {
|
||||
avgResponseTime = float64(0)
|
||||
if numRequests != 0 {
|
||||
avgResponseTime = float64(totalResponseTime) / float64(numRequests)
|
||||
}
|
||||
return avgResponseTime
|
||||
}
|
||||
|
||||
func getAvgContentLength(numRequests int64, totalContentLength int64) (avgContentLength int64) {
|
||||
avgContentLength = int64(0)
|
||||
if numRequests != 0 {
|
||||
avgContentLength = totalContentLength / numRequests
|
||||
}
|
||||
return avgContentLength
|
||||
}
|
||||
|
||||
func getCurrentRps(numRequests int64, duration float64) (currentRps float64) {
|
||||
currentRps = float64(numRequests) / duration
|
||||
return currentRps
|
||||
}
|
||||
|
||||
func getCurrentFailPerSec(numFailures int64, duration float64) (currentFailPerSec float64) {
|
||||
currentFailPerSec = float64(numFailures) / duration
|
||||
return currentFailPerSec
|
||||
}
|
||||
|
||||
func getTotalFailRatio(totalRequests, totalFailures int64) (failRatio float64) {
|
||||
if totalRequests == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(totalFailures) / float64(totalRequests)
|
||||
}
|
||||
|
||||
// OnStart of ConsoleOutput has nothing to do.
|
||||
func (o *ConsoleOutput) OnStart() {
|
||||
}
|
||||
|
||||
// OnStop of ConsoleOutput has nothing to do.
|
||||
func (o *ConsoleOutput) OnStop() {
|
||||
}
|
||||
|
||||
// OnEvent will print to the console.
|
||||
func (o *ConsoleOutput) OnEvent(data map[string]interface{}) {
|
||||
output, err := convertData(data)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to convert data")
|
||||
return
|
||||
}
|
||||
|
||||
state := getStateName(output.State)
|
||||
|
||||
currentTime := time.Now()
|
||||
println(fmt.Sprintf("Current time: %s, Users: %d, State: %s, Total RPS: %.1f, Total Average Response Time: %.1fms, Total Fail Ratio: %.1f%%",
|
||||
currentTime.Format("2006/01/02 15:04:05"), output.UserCount, state, output.TotalRPS, output.TotalAvgResponseTime, output.TotalFailRatio*100))
|
||||
println(fmt.Sprintf("Accumulated Transactions: %d Passed, %d Failed",
|
||||
output.TransactionsPassed, output.TransactionsFailed))
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Type", "Name", "# requests", "# fails", "Median", "Average", "Min", "Max", "Content Size", "# reqs/sec", "# fails/sec"})
|
||||
|
||||
for _, stat := range output.Stats {
|
||||
row := make([]string, 11)
|
||||
row[0] = stat.Method
|
||||
row[1] = stat.Name
|
||||
row[2] = strconv.FormatInt(stat.NumRequests, 10)
|
||||
row[3] = strconv.FormatInt(stat.NumFailures, 10)
|
||||
row[4] = strconv.FormatInt(stat.medianResponseTime, 10)
|
||||
row[5] = strconv.FormatFloat(stat.avgResponseTime, 'f', 2, 64)
|
||||
row[6] = strconv.FormatInt(stat.MinResponseTime, 10)
|
||||
row[7] = strconv.FormatInt(stat.MaxResponseTime, 10)
|
||||
row[8] = strconv.FormatInt(stat.avgContentLength, 10)
|
||||
row[9] = strconv.FormatFloat(stat.currentRps, 'f', 2, 64)
|
||||
row[10] = strconv.FormatFloat(stat.currentFailPerSec, 'f', 2, 64)
|
||||
table.Append(row)
|
||||
}
|
||||
table.Render()
|
||||
println()
|
||||
}
|
||||
|
||||
type statsEntryOutput struct {
|
||||
statsEntry
|
||||
|
||||
medianResponseTime int64 // median response time
|
||||
avgResponseTime float64 // average response time, round float to 2 decimal places
|
||||
avgContentLength int64 // average content size
|
||||
currentRps float64 // # reqs/sec
|
||||
currentFailPerSec float64 // # fails/sec
|
||||
duration float64 // the duration of stats
|
||||
}
|
||||
|
||||
type dataOutput struct {
|
||||
UserCount int64 `json:"user_count"`
|
||||
State int32 `json:"state"`
|
||||
TotalStats *statsEntryOutput `json:"stats_total"`
|
||||
TransactionsPassed int64 `json:"transactions_passed"`
|
||||
TransactionsFailed int64 `json:"transactions_failed"`
|
||||
TotalAvgResponseTime float64 `json:"total_avg_response_time"`
|
||||
TotalMinResponseTime float64 `json:"total_min_response_time"`
|
||||
TotalMaxResponseTime float64 `json:"total_max_response_time"`
|
||||
TotalRPS float64 `json:"total_rps"`
|
||||
TotalFailRatio float64 `json:"total_fail_ratio"`
|
||||
TotalFailPerSec float64 `json:"total_fail_per_sec"`
|
||||
Duration float64 `json:"duration"`
|
||||
Stats []*statsEntryOutput `json:"stats"`
|
||||
Errors map[string]map[string]interface{} `json:"errors"`
|
||||
}
|
||||
|
||||
func convertData(data map[string]interface{}) (output *dataOutput, err error) {
|
||||
userCount, ok := data["user_count"].(int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("user_count is not int32")
|
||||
}
|
||||
state, ok := data["state"].(int32)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("state is not int32")
|
||||
}
|
||||
stats, ok := data["stats"].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("stats is not []interface{}")
|
||||
}
|
||||
|
||||
errors := data["errors"].(map[string]map[string]interface{})
|
||||
|
||||
transactions, ok := data["transactions"].(map[string]int64)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("transactions is not map[string]int64")
|
||||
}
|
||||
transactionsPassed := transactions["passed"]
|
||||
transactionsFailed := transactions["failed"]
|
||||
|
||||
// convert stats in total
|
||||
statsTotal, ok := data["stats_total"].(interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("stats_total is not interface{}")
|
||||
}
|
||||
entryTotalOutput, err := deserializeStatsEntry(statsTotal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output = &dataOutput{
|
||||
UserCount: userCount,
|
||||
State: state,
|
||||
Duration: entryTotalOutput.duration,
|
||||
TotalStats: entryTotalOutput,
|
||||
TransactionsPassed: transactionsPassed,
|
||||
TransactionsFailed: transactionsFailed,
|
||||
TotalAvgResponseTime: entryTotalOutput.avgResponseTime,
|
||||
TotalMaxResponseTime: float64(entryTotalOutput.MaxResponseTime),
|
||||
TotalMinResponseTime: float64(entryTotalOutput.MinResponseTime),
|
||||
TotalRPS: entryTotalOutput.currentRps,
|
||||
TotalFailRatio: getTotalFailRatio(entryTotalOutput.NumRequests, entryTotalOutput.NumFailures),
|
||||
TotalFailPerSec: entryTotalOutput.currentFailPerSec,
|
||||
Stats: make([]*statsEntryOutput, 0, len(stats)),
|
||||
Errors: errors,
|
||||
}
|
||||
|
||||
// convert stats
|
||||
for _, stat := range stats {
|
||||
entryOutput, err := deserializeStatsEntry(stat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output.Stats = append(output.Stats, entryOutput)
|
||||
}
|
||||
// sort stats by type
|
||||
sort.Slice(output.Stats, func(i, j int) bool {
|
||||
return output.Stats[i].Method < output.Stats[j].Method
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func deserializeStatsEntry(stat interface{}) (entryOutput *statsEntryOutput, err error) {
|
||||
statBytes, err := json.Marshal(stat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry := statsEntry{}
|
||||
if err = json.Unmarshal(statBytes, &entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var duration float64
|
||||
if entry.Name == "Total" {
|
||||
duration = float64(entry.LastRequestTimestamp-entry.StartTime) / 1e3
|
||||
if duration == 0 {
|
||||
return nil, errors.New("no step specified")
|
||||
}
|
||||
} else {
|
||||
duration = float64(reportStatsInterval / time.Second)
|
||||
}
|
||||
|
||||
numRequests := entry.NumRequests
|
||||
entryOutput = &statsEntryOutput{
|
||||
statsEntry: entry,
|
||||
duration: duration,
|
||||
medianResponseTime: getMedianResponseTime(numRequests, entry.ResponseTimes),
|
||||
avgResponseTime: getAvgResponseTime(numRequests, entry.TotalResponseTime),
|
||||
avgContentLength: getAvgContentLength(numRequests, entry.TotalContentLength),
|
||||
currentRps: getCurrentRps(numRequests, duration),
|
||||
currentFailPerSec: getCurrentFailPerSec(entry.NumFailures, duration),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// gauge vectors for requests
|
||||
var (
|
||||
gaugeNumRequests = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "num_requests",
|
||||
Help: "The number of requests",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeNumFailures = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "num_failures",
|
||||
Help: "The number of failures",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeMedianResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "median_response_time",
|
||||
Help: "The median response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeAverageResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "average_response_time",
|
||||
Help: "The average response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeMinResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "min_response_time",
|
||||
Help: "The min response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeMaxResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "max_response_time",
|
||||
Help: "The max response time",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeAverageContentLength = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "average_content_length",
|
||||
Help: "The average content length",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeCurrentRPS = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "current_rps",
|
||||
Help: "The current requests per second",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeCurrentFailPerSec = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "current_fail_per_sec",
|
||||
Help: "The current failure number per second",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
)
|
||||
|
||||
// counter for total
|
||||
var (
|
||||
counterErrors = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "errors",
|
||||
Help: "The errors of load testing",
|
||||
},
|
||||
[]string{"method", "name", "error"},
|
||||
)
|
||||
counterTotalNumRequests = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_num_requests",
|
||||
Help: "The number of requests in total",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
counterTotalNumFailures = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_num_failures",
|
||||
Help: "The number of failures in total",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
)
|
||||
|
||||
// summary for total
|
||||
var (
|
||||
summaryResponseTime = prometheus.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Name: "response_time",
|
||||
Help: "The summary of response time",
|
||||
Objectives: map[float64]float64{
|
||||
0.5: 0.01, // PCT50
|
||||
0.9: 0.01, // PCT90
|
||||
0.95: 0.005, // PCT95
|
||||
},
|
||||
AgeBuckets: 1,
|
||||
MaxAge: 100000 * time.Second,
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
)
|
||||
|
||||
// gauges for total
|
||||
var (
|
||||
gaugeUsers = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "users",
|
||||
Help: "The current number of users",
|
||||
},
|
||||
)
|
||||
gaugeState = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "state",
|
||||
Help: "The current runner state, 1=initializing, 2=spawning, 3=running, 4=stopping, 5=stopped, 6=quitting, 7=missing",
|
||||
},
|
||||
)
|
||||
gaugeDuration = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "duration",
|
||||
Help: "The duration of load testing",
|
||||
},
|
||||
)
|
||||
gaugeTotalAverageResponseTime = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_average_response_time",
|
||||
Help: "The average response time in total milliseconds",
|
||||
},
|
||||
)
|
||||
gaugeTotalMinResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_min_response_time",
|
||||
Help: "The min response time in total milliseconds",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeTotalMaxResponseTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_max_response_time",
|
||||
Help: "The max response time in total milliseconds",
|
||||
},
|
||||
[]string{"method", "name"},
|
||||
)
|
||||
gaugeTotalRPS = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_rps",
|
||||
Help: "The requests per second in total",
|
||||
},
|
||||
)
|
||||
gaugeTotalFailRatio = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "fail_ratio",
|
||||
Help: "The ratio of request failures in total",
|
||||
},
|
||||
)
|
||||
gaugeTotalFailPerSec = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "total_fail_per_sec",
|
||||
Help: "The failure number per second in total",
|
||||
},
|
||||
)
|
||||
gaugeTransactionsPassed = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "transactions_passed",
|
||||
Help: "The accumulated number of passed transactions",
|
||||
},
|
||||
)
|
||||
gaugeTransactionsFailed = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "transactions_failed",
|
||||
Help: "The accumulated number of failed transactions",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
var (
|
||||
minResponseTimeMap = sync.Map{}
|
||||
maxResponseTimeMap = sync.Map{}
|
||||
)
|
||||
|
||||
// NewPrometheusPusherOutput returns a PrometheusPusherOutput.
|
||||
func NewPrometheusPusherOutput(gatewayURL, jobName string, mode string) *PrometheusPusherOutput {
|
||||
nodeUUID, _ := uuid.NewUUID()
|
||||
return &PrometheusPusherOutput{
|
||||
pusher: push.New(gatewayURL, jobName).
|
||||
Grouping("instance", nodeUUID.String()).
|
||||
Grouping("mode", mode),
|
||||
}
|
||||
}
|
||||
|
||||
// PrometheusPusherOutput pushes boomer stats to Prometheus Pushgateway.
|
||||
type PrometheusPusherOutput struct {
|
||||
pusher *push.Pusher // Prometheus Pushgateway Pusher
|
||||
}
|
||||
|
||||
// OnStart will register all prometheus metric collectors
|
||||
func (o *PrometheusPusherOutput) OnStart() {
|
||||
// reset all prometheus metrics
|
||||
resetPrometheusMetrics()
|
||||
log.Info().Msg("register prometheus metric collectors")
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(
|
||||
// gauge vectors for requests
|
||||
gaugeNumRequests,
|
||||
gaugeNumFailures,
|
||||
gaugeMedianResponseTime,
|
||||
gaugeAverageResponseTime,
|
||||
gaugeMinResponseTime,
|
||||
gaugeMaxResponseTime,
|
||||
gaugeAverageContentLength,
|
||||
gaugeCurrentRPS,
|
||||
gaugeCurrentFailPerSec,
|
||||
// counter for total
|
||||
counterErrors,
|
||||
counterTotalNumRequests,
|
||||
counterTotalNumFailures,
|
||||
// summary for total
|
||||
summaryResponseTime,
|
||||
// gauges for total
|
||||
gaugeUsers,
|
||||
gaugeState,
|
||||
gaugeDuration,
|
||||
gaugeTotalAverageResponseTime,
|
||||
gaugeTotalMinResponseTime,
|
||||
gaugeTotalMaxResponseTime,
|
||||
gaugeTotalRPS,
|
||||
gaugeTotalFailRatio,
|
||||
gaugeTotalFailPerSec,
|
||||
gaugeTransactionsPassed,
|
||||
gaugeTransactionsFailed,
|
||||
)
|
||||
o.pusher = o.pusher.Gatherer(registry)
|
||||
}
|
||||
|
||||
// OnStop of PrometheusPusherOutput has nothing to do.
|
||||
func (o *PrometheusPusherOutput) OnStop() {
|
||||
// update runner state: stopped
|
||||
gaugeState.Set(float64(StateStopped))
|
||||
if err := o.pusher.Push(); err != nil {
|
||||
log.Error().Err(err).Msg("push to Pushgateway failed")
|
||||
}
|
||||
}
|
||||
|
||||
// OnEvent will push metric to Prometheus Pushgataway
|
||||
func (o *PrometheusPusherOutput) OnEvent(data map[string]interface{}) {
|
||||
output, err := convertData(data)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to convert data")
|
||||
return
|
||||
}
|
||||
|
||||
// user count
|
||||
gaugeUsers.Set(float64(output.UserCount))
|
||||
|
||||
// runner state
|
||||
gaugeState.Set(float64(output.State))
|
||||
|
||||
// min/avg/max response time in total
|
||||
gaugeTotalAverageResponseTime.Set(output.TotalAvgResponseTime)
|
||||
gaugeTotalMinResponseTime.WithLabelValues("", "Total").Set(output.TotalMinResponseTime)
|
||||
gaugeTotalMaxResponseTime.WithLabelValues("", "Total").Set(output.TotalMaxResponseTime)
|
||||
|
||||
// duration
|
||||
gaugeDuration.Set(output.Duration)
|
||||
|
||||
// rps in total
|
||||
gaugeTotalRPS.Set(output.TotalRPS)
|
||||
|
||||
// failure ratio in total
|
||||
gaugeTotalFailRatio.Set(output.TotalFailRatio)
|
||||
|
||||
// failure per second in total
|
||||
gaugeTotalFailPerSec.Set(output.TotalFailPerSec)
|
||||
|
||||
// accumulated number of transactions
|
||||
gaugeTransactionsPassed.Set(float64(output.TransactionsPassed))
|
||||
gaugeTransactionsFailed.Set(float64(output.TransactionsFailed))
|
||||
|
||||
for _, stat := range output.Stats {
|
||||
method := stat.Method
|
||||
name := stat.Name
|
||||
// stats in stats interval
|
||||
gaugeNumRequests.WithLabelValues(method, name).Set(float64(stat.NumRequests))
|
||||
gaugeNumFailures.WithLabelValues(method, name).Set(float64(stat.NumFailures))
|
||||
gaugeMedianResponseTime.WithLabelValues(method, name).Set(float64(stat.medianResponseTime))
|
||||
gaugeAverageResponseTime.WithLabelValues(method, name).Set(float64(stat.avgResponseTime))
|
||||
gaugeMinResponseTime.WithLabelValues(method, name).Set(float64(stat.MinResponseTime))
|
||||
gaugeMaxResponseTime.WithLabelValues(method, name).Set(float64(stat.MaxResponseTime))
|
||||
gaugeAverageContentLength.WithLabelValues(method, name).Set(float64(stat.avgContentLength))
|
||||
gaugeCurrentRPS.WithLabelValues(method, name).Set(stat.currentRps)
|
||||
gaugeCurrentFailPerSec.WithLabelValues(method, name).Set(stat.currentFailPerSec)
|
||||
for responseTime, count := range stat.ResponseTimes {
|
||||
var i int64
|
||||
for i = 0; i < count; i++ {
|
||||
summaryResponseTime.WithLabelValues(method, name).Observe(float64(responseTime))
|
||||
}
|
||||
}
|
||||
// every stat in total
|
||||
key := fmt.Sprintf("%v_%v", method, name)
|
||||
minResponseTime, loaded := minResponseTimeMap.LoadOrStore(key, float64(stat.MinResponseTime))
|
||||
if loaded {
|
||||
minResponseTime = math.Min(minResponseTime.(float64), float64(stat.MinResponseTime))
|
||||
minResponseTimeMap.Store(key, minResponseTime)
|
||||
}
|
||||
gaugeTotalMinResponseTime.WithLabelValues(method, name).Set(minResponseTime.(float64))
|
||||
|
||||
maxResponseTime, loaded := maxResponseTimeMap.LoadOrStore(key, float64(stat.MaxResponseTime))
|
||||
if loaded {
|
||||
maxResponseTime = math.Max(maxResponseTime.(float64), float64(stat.MaxResponseTime))
|
||||
maxResponseTimeMap.Store(key, maxResponseTime)
|
||||
}
|
||||
gaugeTotalMaxResponseTime.WithLabelValues(method, name).Set(maxResponseTime.(float64))
|
||||
|
||||
counterTotalNumRequests.WithLabelValues(method, name).Add(float64(stat.NumRequests))
|
||||
counterTotalNumFailures.WithLabelValues(method, name).Add(float64(stat.NumFailures))
|
||||
}
|
||||
|
||||
// errors
|
||||
for _, requestError := range output.Errors {
|
||||
counterErrors.WithLabelValues(
|
||||
requestError["method"].(string),
|
||||
requestError["name"].(string),
|
||||
requestError["error"].(string),
|
||||
).Add(float64(requestError["occurrences"].(int64)))
|
||||
}
|
||||
|
||||
if err := o.pusher.Push(); err != nil {
|
||||
log.Error().Err(err).Msg("push to Pushgateway failed")
|
||||
}
|
||||
}
|
||||
|
||||
// resetPrometheusMetrics will reset all metrics
|
||||
func resetPrometheusMetrics() {
|
||||
log.Info().Msg("reset all prometheus metrics")
|
||||
gaugeNumRequests.Reset()
|
||||
gaugeNumFailures.Reset()
|
||||
gaugeMedianResponseTime.Reset()
|
||||
gaugeAverageResponseTime.Reset()
|
||||
gaugeMinResponseTime.Reset()
|
||||
gaugeMaxResponseTime.Reset()
|
||||
gaugeAverageContentLength.Reset()
|
||||
gaugeCurrentRPS.Reset()
|
||||
gaugeCurrentFailPerSec.Reset()
|
||||
// counter for total
|
||||
counterErrors.Reset()
|
||||
counterTotalNumRequests.Reset()
|
||||
counterTotalNumFailures.Reset()
|
||||
// summary for total
|
||||
summaryResponseTime.Reset()
|
||||
// gauges for total
|
||||
gaugeUsers.Set(0)
|
||||
gaugeState.Set(1)
|
||||
gaugeDuration.Set(0)
|
||||
gaugeTotalAverageResponseTime.Set(0)
|
||||
gaugeTotalMinResponseTime.Reset()
|
||||
gaugeTotalMaxResponseTime.Reset()
|
||||
gaugeTotalRPS.Set(0)
|
||||
gaugeTotalFailRatio.Set(0)
|
||||
gaugeTotalFailPerSec.Set(0)
|
||||
gaugeTransactionsPassed.Set(0)
|
||||
gaugeTransactionsFailed.Set(0)
|
||||
|
||||
minResponseTimeMap = sync.Map{}
|
||||
maxResponseTimeMap = sync.Map{}
|
||||
}
|
||||
104
hrp/pkg/boomer/output_test.go
Normal file
104
hrp/pkg/boomer/output_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetMedianResponseTime(t *testing.T) {
|
||||
numRequests := int64(10)
|
||||
responseTimes := map[int64]int64{
|
||||
100: 1,
|
||||
200: 3,
|
||||
300: 6,
|
||||
}
|
||||
|
||||
medianResponseTime := getMedianResponseTime(numRequests, responseTimes)
|
||||
if medianResponseTime != 300 {
|
||||
t.Error("medianResponseTime should be 300")
|
||||
}
|
||||
|
||||
responseTimes = map[int64]int64{}
|
||||
|
||||
medianResponseTime = getMedianResponseTime(numRequests, responseTimes)
|
||||
if medianResponseTime != 0 {
|
||||
t.Error("medianResponseTime should be 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAvgResponseTime(t *testing.T) {
|
||||
numRequests := int64(3)
|
||||
totalResponseTime := int64(100)
|
||||
|
||||
avgResponseTime := getAvgResponseTime(numRequests, totalResponseTime)
|
||||
if math.Dim(float64(33.33), avgResponseTime) > 0.01 {
|
||||
t.Error("avgResponseTime should be close to 33.33")
|
||||
}
|
||||
|
||||
avgResponseTime = getAvgResponseTime(int64(0), totalResponseTime)
|
||||
if avgResponseTime != float64(0) {
|
||||
t.Error("avgResponseTime should be close to 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAvgContentLength(t *testing.T) {
|
||||
numRequests := int64(3)
|
||||
totalContentLength := int64(100)
|
||||
|
||||
avgContentLength := getAvgContentLength(numRequests, totalContentLength)
|
||||
if avgContentLength != 33 {
|
||||
t.Error("avgContentLength should be 33")
|
||||
}
|
||||
|
||||
avgContentLength = getAvgContentLength(int64(0), totalContentLength)
|
||||
if avgContentLength != 0 {
|
||||
t.Error("avgContentLength should be 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCurrentRps(t *testing.T) {
|
||||
duration := float64(3)
|
||||
numRequests := int64(6)
|
||||
currentRps := getCurrentRps(numRequests, duration)
|
||||
if currentRps != 2 {
|
||||
t.Error("currentRps should be 2")
|
||||
}
|
||||
|
||||
numRequests = int64(8)
|
||||
currentRps = getCurrentRps(numRequests, duration)
|
||||
if fmt.Sprintf("%.2f", currentRps) != "2.67" {
|
||||
t.Error("currentRps should be 2.67")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleOutput(t *testing.T) {
|
||||
o := NewConsoleOutput()
|
||||
o.OnStart()
|
||||
|
||||
data := map[string]interface{}{}
|
||||
stat := map[string]interface{}{}
|
||||
data["stats"] = []interface{}{stat}
|
||||
|
||||
stat["name"] = "http"
|
||||
stat["method"] = "post"
|
||||
stat["num_requests"] = int64(100)
|
||||
stat["num_failures"] = int64(10)
|
||||
stat["response_times"] = map[int64]int64{
|
||||
10: 1,
|
||||
100: 99,
|
||||
}
|
||||
stat["total_response_time"] = int64(9910)
|
||||
stat["min_response_time"] = int64(10)
|
||||
stat["max_response_time"] = int64(100)
|
||||
stat["total_content_length"] = int64(100000)
|
||||
stat["num_reqs_per_sec"] = map[int64]int64{
|
||||
1: 20,
|
||||
2: 40,
|
||||
3: 40,
|
||||
}
|
||||
|
||||
o.OnEvent(data)
|
||||
|
||||
o.OnStop()
|
||||
}
|
||||
230
hrp/pkg/boomer/ratelimiter.go
Normal file
230
hrp/pkg/boomer/ratelimiter.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RateLimiter is used to put limits on task executions.
|
||||
type RateLimiter interface {
|
||||
// Start is used to enable the rate limiter.
|
||||
// It can be implemented as a noop if not needed.
|
||||
Start()
|
||||
|
||||
// Acquire() is called before executing a task.Fn function.
|
||||
// If Acquire() returns true, the task.Fn function will be executed.
|
||||
// If Acquire() returns false, the task.Fn function won't be executed this time, but Acquire() will be called very soon.
|
||||
// It works like:
|
||||
// for {
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if !blocked {
|
||||
// task.Fn()
|
||||
// }
|
||||
// }
|
||||
// Acquire() should block the caller until execution is allowed.
|
||||
Acquire() bool
|
||||
|
||||
// Stop is used to disable the rate limiter.
|
||||
// It can be implemented as a noop if not needed.
|
||||
Stop()
|
||||
}
|
||||
|
||||
// A StableRateLimiter uses the token bucket algorithm.
|
||||
// the bucket is refilled according to the refill period, no burst is allowed.
|
||||
type StableRateLimiter struct {
|
||||
threshold int64
|
||||
currentThreshold int64
|
||||
refillPeriod time.Duration
|
||||
broadcastChanMux *sync.RWMutex // avoid data race
|
||||
broadcastChannel chan bool
|
||||
quitChannel chan bool
|
||||
}
|
||||
|
||||
// NewStableRateLimiter returns a StableRateLimiter.
|
||||
func NewStableRateLimiter(threshold int64, refillPeriod time.Duration) (rateLimiter *StableRateLimiter) {
|
||||
rateLimiter = &StableRateLimiter{
|
||||
threshold: threshold,
|
||||
currentThreshold: threshold,
|
||||
refillPeriod: refillPeriod,
|
||||
broadcastChanMux: new(sync.RWMutex),
|
||||
broadcastChannel: make(chan bool),
|
||||
}
|
||||
return rateLimiter
|
||||
}
|
||||
|
||||
// Start to refill the bucket periodically.
|
||||
func (limiter *StableRateLimiter) Start() {
|
||||
limiter.quitChannel = make(chan bool)
|
||||
quitChannel := limiter.quitChannel
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
return
|
||||
default:
|
||||
atomic.StoreInt64(&limiter.currentThreshold, limiter.threshold)
|
||||
time.Sleep(limiter.refillPeriod)
|
||||
close(limiter.broadcastChannel)
|
||||
// avoid data race
|
||||
limiter.broadcastChanMux.Lock()
|
||||
limiter.broadcastChannel = make(chan bool)
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Acquire a token from the bucket, returns true if the bucket is exhausted.
|
||||
func (limiter *StableRateLimiter) Acquire() (blocked bool) {
|
||||
permit := atomic.AddInt64(&limiter.currentThreshold, -1)
|
||||
if permit < 0 {
|
||||
blocked = true
|
||||
// block until the bucket is refilled
|
||||
limiter.broadcastChanMux.Lock()
|
||||
<-limiter.broadcastChannel
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
} else {
|
||||
blocked = false
|
||||
}
|
||||
return blocked
|
||||
}
|
||||
|
||||
// Stop the rate limiter.
|
||||
func (limiter *StableRateLimiter) Stop() {
|
||||
close(limiter.quitChannel)
|
||||
}
|
||||
|
||||
// ErrParsingRampUpRate is the error returned if the format of rampUpRate is invalid.
|
||||
var ErrParsingRampUpRate = errors.New("ratelimiter: invalid format of rampUpRate, try \"1\" or \"1/1s\"")
|
||||
|
||||
// A RampUpRateLimiter uses the token bucket algorithm.
|
||||
// the threshold is updated according to the warm up rate.
|
||||
// the bucket is refilled according to the refill period, no burst is allowed.
|
||||
type RampUpRateLimiter struct {
|
||||
maxThreshold int64
|
||||
nextThreshold int64
|
||||
currentThreshold int64
|
||||
refillPeriod time.Duration
|
||||
rampUpRate string
|
||||
rampUpStep int64
|
||||
rampUpPeroid time.Duration
|
||||
|
||||
broadcastChanMux *sync.RWMutex // avoid data race
|
||||
broadcastChannel chan bool
|
||||
|
||||
rampUpChannel chan bool
|
||||
quitChannel chan bool
|
||||
}
|
||||
|
||||
// NewRampUpRateLimiter returns a RampUpRateLimiter.
|
||||
// Valid formats of rampUpRate are "1", "1/1s".
|
||||
func NewRampUpRateLimiter(maxThreshold int64, rampUpRate string, refillPeriod time.Duration) (rateLimiter *RampUpRateLimiter, err error) {
|
||||
rateLimiter = &RampUpRateLimiter{
|
||||
maxThreshold: maxThreshold,
|
||||
nextThreshold: 0,
|
||||
currentThreshold: 0,
|
||||
rampUpRate: rampUpRate,
|
||||
refillPeriod: refillPeriod,
|
||||
broadcastChanMux: new(sync.RWMutex),
|
||||
broadcastChannel: make(chan bool),
|
||||
}
|
||||
rateLimiter.rampUpStep, rateLimiter.rampUpPeroid, err = rateLimiter.parseRampUpRate(rateLimiter.rampUpRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rateLimiter, nil
|
||||
}
|
||||
|
||||
func (limiter *RampUpRateLimiter) parseRampUpRate(rampUpRate string) (rampUpStep int64, rampUpPeroid time.Duration, err error) {
|
||||
if strings.Contains(rampUpRate, "/") {
|
||||
tmp := strings.Split(rampUpRate, "/")
|
||||
if len(tmp) != 2 {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
rampUpStep, err := strconv.ParseInt(tmp[0], 10, 64)
|
||||
if err != nil {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
rampUpPeroid, err := time.ParseDuration(tmp[1])
|
||||
if err != nil {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
return rampUpStep, rampUpPeroid, nil
|
||||
}
|
||||
|
||||
rampUpStep, err = strconv.ParseInt(rampUpRate, 10, 64)
|
||||
if err != nil {
|
||||
return rampUpStep, rampUpPeroid, ErrParsingRampUpRate
|
||||
}
|
||||
rampUpPeroid = time.Second
|
||||
return rampUpStep, rampUpPeroid, nil
|
||||
}
|
||||
|
||||
// Start to refill the bucket periodically.
|
||||
func (limiter *RampUpRateLimiter) Start() {
|
||||
limiter.quitChannel = make(chan bool)
|
||||
quitChannel := limiter.quitChannel
|
||||
// bucket updater
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
return
|
||||
default:
|
||||
atomic.StoreInt64(&limiter.currentThreshold, atomic.LoadInt64(&limiter.nextThreshold))
|
||||
time.Sleep(limiter.refillPeriod)
|
||||
close(limiter.broadcastChannel)
|
||||
// avoid data race
|
||||
limiter.broadcastChanMux.Lock()
|
||||
limiter.broadcastChannel = make(chan bool)
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
// threshold updater
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
return
|
||||
default:
|
||||
nextValue := atomic.LoadInt64(&limiter.nextThreshold) + limiter.rampUpStep
|
||||
if nextValue < 0 {
|
||||
// int64 overflow
|
||||
nextValue = int64(math.MaxInt64)
|
||||
}
|
||||
if nextValue > limiter.maxThreshold {
|
||||
nextValue = limiter.maxThreshold
|
||||
}
|
||||
atomic.StoreInt64(&limiter.nextThreshold, nextValue)
|
||||
time.Sleep(limiter.rampUpPeroid)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Acquire a token from the bucket, returns true if the bucket is exhausted.
|
||||
func (limiter *RampUpRateLimiter) Acquire() (blocked bool) {
|
||||
permit := atomic.AddInt64(&limiter.currentThreshold, -1)
|
||||
if permit < 0 {
|
||||
blocked = true
|
||||
// block until the bucket is refilled
|
||||
limiter.broadcastChanMux.Lock()
|
||||
<-limiter.broadcastChannel
|
||||
limiter.broadcastChanMux.Unlock()
|
||||
} else {
|
||||
blocked = false
|
||||
}
|
||||
return blocked
|
||||
}
|
||||
|
||||
// Stop the rate limiter.
|
||||
func (limiter *RampUpRateLimiter) Stop() {
|
||||
atomic.StoreInt64(&limiter.nextThreshold, 0)
|
||||
close(limiter.quitChannel)
|
||||
}
|
||||
102
hrp/pkg/boomer/ratelimiter_test.go
Normal file
102
hrp/pkg/boomer/ratelimiter_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestStableRateLimiter(t *testing.T) {
|
||||
rateLimiter := NewStableRateLimiter(1, 10*time.Millisecond)
|
||||
rateLimiter.Start()
|
||||
defer rateLimiter.Stop()
|
||||
|
||||
blocked := rateLimiter.Acquire()
|
||||
if blocked {
|
||||
t.Error("Unexpected blocked by rate limiter")
|
||||
}
|
||||
blocked = rateLimiter.Acquire()
|
||||
if !blocked {
|
||||
t.Error("Should be blocked")
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME
|
||||
// func TestRampUpRateLimiter(t *testing.T) {
|
||||
// rateLimiter, _ := NewRampUpRateLimiter(100, "10/200ms", 100*time.Millisecond)
|
||||
// rateLimiter.Start()
|
||||
// defer rateLimiter.Stop()
|
||||
|
||||
// time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// for i := 0; i < 10; i++ {
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if blocked {
|
||||
// t.Fatal("Unexpected blocked by rate limiter")
|
||||
// }
|
||||
// }
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if !blocked {
|
||||
// t.Fatal("Should be blocked")
|
||||
// }
|
||||
|
||||
// time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// // now, the threshold is 20
|
||||
// for i := 0; i < 20; i++ {
|
||||
// blocked := rateLimiter.Acquire()
|
||||
// if blocked {
|
||||
// t.Fatal("Unexpected blocked by rate limiter")
|
||||
// }
|
||||
// }
|
||||
// blocked = rateLimiter.Acquire()
|
||||
// if !blocked {
|
||||
// t.Fatal("Should be blocked")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestParseRampUpRate(t *testing.T) {
|
||||
rateLimiter := &RampUpRateLimiter{}
|
||||
rampUpStep, rampUpPeriod, _ := rateLimiter.parseRampUpRate("100")
|
||||
if rampUpStep != 100 {
|
||||
t.Error("Wrong rampUpStep, expected: 100, was:", rampUpStep)
|
||||
}
|
||||
if rampUpPeriod != time.Second {
|
||||
t.Error("Wrong rampUpPeriod, expected: 1s, was:", rampUpPeriod)
|
||||
}
|
||||
rampUpStep, rampUpPeriod, _ = rateLimiter.parseRampUpRate("200/10s")
|
||||
if rampUpStep != 200 {
|
||||
t.Error("Wrong rampUpStep, expected: 200, was:", rampUpStep)
|
||||
}
|
||||
if rampUpPeriod != 10*time.Second {
|
||||
t.Error("Wrong rampUpPeriod, expected: 10s, was:", rampUpPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInvalidRampUpRate(t *testing.T) {
|
||||
rateLimiter := &RampUpRateLimiter{}
|
||||
|
||||
_, _, err := rateLimiter.parseRampUpRate("A/1m")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
_, _, err = rateLimiter.parseRampUpRate("A")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
_, _, err = rateLimiter.parseRampUpRate("200/1s/")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
_, _, err = rateLimiter.parseRampUpRate("200/1")
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
|
||||
rateLimiter, err = NewRampUpRateLimiter(1, "200/1", time.Second)
|
||||
if err == nil || err != ErrParsingRampUpRate {
|
||||
t.Error("Expected ErrParsingRampUpRate")
|
||||
}
|
||||
}
|
||||
1417
hrp/pkg/boomer/runner.go
Normal file
1417
hrp/pkg/boomer/runner.go
Normal file
File diff suppressed because it is too large
Load Diff
552
hrp/pkg/boomer/runner_test.go
Normal file
552
hrp/pkg/boomer/runner_test.go
Normal file
@@ -0,0 +1,552 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager"
|
||||
)
|
||||
|
||||
type HitOutput struct {
|
||||
onStart bool
|
||||
onEvent bool
|
||||
onStop bool
|
||||
}
|
||||
|
||||
func (o *HitOutput) OnStart() {
|
||||
o.onStart = true
|
||||
}
|
||||
|
||||
func (o *HitOutput) OnEvent(data map[string]interface{}) {
|
||||
o.onEvent = true
|
||||
}
|
||||
|
||||
func (o *HitOutput) OnStop() {
|
||||
o.onStop = true
|
||||
}
|
||||
|
||||
func TestSafeRun(t *testing.T) {
|
||||
runner := &runner{}
|
||||
runner.safeRun(func() {
|
||||
panic("Runner will catch this panic")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOutputOnStart(t *testing.T) {
|
||||
hitOutput := &HitOutput{}
|
||||
hitOutput2 := &HitOutput{}
|
||||
runner := &runner{}
|
||||
runner.addOutput(hitOutput)
|
||||
runner.addOutput(hitOutput2)
|
||||
runner.outputOnStart()
|
||||
if !hitOutput.onStart {
|
||||
t.Error("hitOutput's OnStart has not been called")
|
||||
}
|
||||
if !hitOutput2.onStart {
|
||||
t.Error("hitOutput2's OnStart has not been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputOnEvent(t *testing.T) {
|
||||
hitOutput := &HitOutput{}
|
||||
hitOutput2 := &HitOutput{}
|
||||
runner := &runner{}
|
||||
runner.addOutput(hitOutput)
|
||||
runner.addOutput(hitOutput2)
|
||||
runner.outputOnEvent(nil)
|
||||
if !hitOutput.onEvent {
|
||||
t.Error("hitOutput's OnEvent has not been called")
|
||||
}
|
||||
if !hitOutput2.onEvent {
|
||||
t.Error("hitOutput2's OnEvent has not been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOutputOnStop(t *testing.T) {
|
||||
hitOutput := &HitOutput{}
|
||||
hitOutput2 := &HitOutput{}
|
||||
runner := &runner{}
|
||||
runner.addOutput(hitOutput)
|
||||
runner.addOutput(hitOutput2)
|
||||
runner.outputOnStop()
|
||||
if !hitOutput.onStop {
|
||||
t.Error("hitOutput's OnStop has not been called")
|
||||
}
|
||||
if !hitOutput2.onStop {
|
||||
t.Error("hitOutput2's OnStop has not been called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRunner(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Weight: 10,
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
Name: "TaskA",
|
||||
}
|
||||
tasks := []*Task{taskA}
|
||||
runner := newLocalRunner(2, 2)
|
||||
runner.setTasks(tasks)
|
||||
go runner.start()
|
||||
time.Sleep(4 * time.Second)
|
||||
runner.stop()
|
||||
}
|
||||
|
||||
func TestLoopCount(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Weight: 10,
|
||||
Fn: func() {
|
||||
time.Sleep(time.Millisecond)
|
||||
},
|
||||
Name: "TaskA",
|
||||
}
|
||||
tasks := []*Task{taskA}
|
||||
runner := newLocalRunner(2, 2)
|
||||
runner.loop = &Loop{loopCount: 4}
|
||||
runner.setTasks(tasks)
|
||||
runner.start()
|
||||
if !assert.Equal(t, atomic.LoadInt64(&runner.loop.loopCount), atomic.LoadInt64(&runner.loop.finishedCount)) {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopNotify(t *testing.T) {
|
||||
r := &localRunner{
|
||||
runner: runner{
|
||||
stopChan: make(chan bool),
|
||||
doneChan: make(chan bool),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
<-r.stopChan
|
||||
close(r.doneChan)
|
||||
}()
|
||||
|
||||
notifier := r.stopNotify()
|
||||
select {
|
||||
case <-notifier:
|
||||
t.Fatalf("received unexpected stop notification")
|
||||
default:
|
||||
}
|
||||
r.gracefulStop()
|
||||
select {
|
||||
case <-notifier:
|
||||
default:
|
||||
t.Fatalf("cannot receive stop notification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnWorkers(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Weight: 10,
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
Name: "TaskA",
|
||||
}
|
||||
tasks := []*Task{taskA}
|
||||
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
defer runner.close()
|
||||
|
||||
runner.client = newClient("localhost", 5557, runner.nodeID)
|
||||
runner.reset()
|
||||
runner.setTasks(tasks)
|
||||
go runner.spawnWorkers(10, 10, runner.stopChan, runner.spawnComplete)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
currentClients := runner.controller.getCurrentClientsNum()
|
||||
if currentClients != 10 {
|
||||
t.Error("Unexpected count", currentClients)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnWorkersWithManyTasks(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
taskCalls := map[string]int{}
|
||||
|
||||
createTask := func(name string, weight int) *Task {
|
||||
return &Task{
|
||||
Name: name,
|
||||
Weight: weight,
|
||||
Fn: func() {
|
||||
lock.Lock()
|
||||
taskCalls[name]++
|
||||
lock.Unlock()
|
||||
},
|
||||
}
|
||||
}
|
||||
tasks := []*Task{
|
||||
createTask("one hundred", 100),
|
||||
createTask("ten", 10),
|
||||
createTask("one", 1),
|
||||
}
|
||||
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
defer runner.close()
|
||||
|
||||
runner.reset()
|
||||
runner.setTasks(tasks)
|
||||
runner.client = newClient("localhost", 5557, runner.nodeID)
|
||||
|
||||
const numToSpawn int64 = 20
|
||||
|
||||
go runner.spawnWorkers(numToSpawn, float64(numToSpawn), runner.stopChan, runner.spawnComplete)
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
currentClients := runner.controller.getCurrentClientsNum()
|
||||
|
||||
assert.Equal(t, numToSpawn, int64(currentClients))
|
||||
lock.Lock()
|
||||
hundreds := taskCalls["one hundred"]
|
||||
tens := taskCalls["ten"]
|
||||
ones := taskCalls["one"]
|
||||
lock.Unlock()
|
||||
|
||||
total := hundreds + tens + ones
|
||||
t.Logf("total tasks run: %d\n", total)
|
||||
|
||||
assert.True(t, total > 111)
|
||||
|
||||
assert.True(t, ones > 1)
|
||||
actPercentage := float64(ones) / float64(total)
|
||||
expectedPercentage := 1.0 / 111.0
|
||||
if actPercentage > 2*expectedPercentage || actPercentage < 0.5*expectedPercentage {
|
||||
t.Errorf("Unexpected percentage of ones task: exp %v, act %v", expectedPercentage, actPercentage)
|
||||
}
|
||||
|
||||
assert.True(t, tens > 10)
|
||||
actPercentage = float64(tens) / float64(total)
|
||||
expectedPercentage = 10.0 / 111.0
|
||||
if actPercentage > 2*expectedPercentage || actPercentage < 0.5*expectedPercentage {
|
||||
t.Errorf("Unexpected percentage of tens task: exp %v, act %v", expectedPercentage, actPercentage)
|
||||
}
|
||||
|
||||
assert.True(t, hundreds > 100)
|
||||
actPercentage = float64(hundreds) / float64(total)
|
||||
expectedPercentage = 100.0 / 111.0
|
||||
if actPercentage > 2*expectedPercentage || actPercentage < 0.5*expectedPercentage {
|
||||
t.Errorf("Unexpected percentage of hundreds task: exp %v, act %v", expectedPercentage, actPercentage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpawnAndStop(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
}
|
||||
taskB := &Task{
|
||||
Fn: func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
},
|
||||
}
|
||||
tasks := []*Task{taskA, taskB}
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
defer runner.close()
|
||||
runner.client = newClient("localhost", 5557, runner.nodeID)
|
||||
|
||||
runner.setTasks(tasks)
|
||||
runner.setSpawnCount(10)
|
||||
runner.setSpawnRate(10)
|
||||
|
||||
go runner.start()
|
||||
|
||||
// wait for spawning goroutines
|
||||
time.Sleep(2 * time.Second)
|
||||
if runner.controller.getCurrentClientsNum() != 10 {
|
||||
t.Error("Number of goroutines mismatches, expected: 10, current count", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
|
||||
msg := <-runner.client.sendChannel()
|
||||
if msg.Type != "spawning_complete" {
|
||||
t.Error("Runner should send spawning_complete message when spawning completed, got", msg.Type)
|
||||
}
|
||||
go runner.stop()
|
||||
close(runner.doneChan)
|
||||
|
||||
runner.onQuiting()
|
||||
msg = <-runner.client.sendChannel()
|
||||
if msg.Type != "quit" {
|
||||
t.Error("Runner should send quit message on quitting, got", msg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
}
|
||||
tasks := []*Task{taskA}
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
runner.setTasks(tasks)
|
||||
runner.reset()
|
||||
runner.updateState(StateSpawning)
|
||||
|
||||
go runner.stop()
|
||||
close(runner.doneChan)
|
||||
time.Sleep(1 * time.Second)
|
||||
if runner.getState() != StateStopped {
|
||||
t.Error("Expected runner state to be 5, was", getStateName(runner.getState()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnSpawnMessage(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
}
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
defer runner.close()
|
||||
runner.client = newClient("localhost", 5557, runner.nodeID)
|
||||
runner.updateState(StateInit)
|
||||
runner.reset()
|
||||
runner.setTasks([]*Task{taskA})
|
||||
runner.setSpawnCount(100)
|
||||
runner.setSpawnRate(100)
|
||||
runner.onSpawnMessage(newMessageToWorker("spawn", ProfileToBytes(&Profile{SpawnCount: 20, SpawnRate: 20}), nil, nil))
|
||||
|
||||
if runner.getSpawnCount() != 20 {
|
||||
t.Error("workers should be overwrote by onSpawnMessage, expected: 20, was:", runner.controller.spawnCount)
|
||||
}
|
||||
if runner.getSpawnRate() != 20 {
|
||||
t.Error("spawnRate should be overwrote by onSpawnMessage, expected: 20, was:", runner.controller.spawnRate)
|
||||
}
|
||||
|
||||
runner.onMessage(newGenericMessage("stop", nil, runner.nodeID))
|
||||
}
|
||||
|
||||
func TestOnQuitMessage(t *testing.T) {
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
runner.client = newClient("localhost", 5557, "test")
|
||||
runner.updateState(StateInit)
|
||||
|
||||
runner.onMessage(newGenericMessage("quit", nil, runner.nodeID))
|
||||
<-runner.closeChan
|
||||
|
||||
runner.updateState(StateRunning)
|
||||
runner.reset()
|
||||
runner.closeChan = make(chan bool)
|
||||
runner.client.shutdownChan = make(chan bool)
|
||||
go runner.onMessage(newGenericMessage("quit", nil, runner.nodeID))
|
||||
close(runner.doneChan)
|
||||
<-runner.closeChan
|
||||
runner.onQuiting()
|
||||
if runner.getState() != StateQuitting {
|
||||
t.Error("Runner's state should be StateQuitting")
|
||||
}
|
||||
|
||||
runner.updateState(StateStopped)
|
||||
runner.closeChan = make(chan bool)
|
||||
runner.reset()
|
||||
runner.client.shutdownChan = make(chan bool)
|
||||
runner.onMessage(newGenericMessage("quit", nil, runner.nodeID))
|
||||
<-runner.closeChan
|
||||
runner.onQuiting()
|
||||
if runner.getState() != StateQuitting {
|
||||
t.Error("Runner's state should be StateQuitting")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnMessage(t *testing.T) {
|
||||
taskA := &Task{
|
||||
Fn: func() {
|
||||
time.Sleep(time.Second)
|
||||
},
|
||||
}
|
||||
taskB := &Task{
|
||||
Fn: func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
},
|
||||
}
|
||||
tasks := []*Task{taskA, taskB}
|
||||
|
||||
runner := newWorkerRunner("localhost", 5557)
|
||||
runner.client = newClient("localhost", 5557, runner.nodeID)
|
||||
runner.updateState(StateInit)
|
||||
runner.setTasks(tasks)
|
||||
|
||||
// start spawning
|
||||
runner.onMessage(newMessageToWorker("spawn", ProfileToBytes(&Profile{SpawnCount: 10, SpawnRate: 10}), nil, nil))
|
||||
go runner.start()
|
||||
|
||||
msg := <-runner.client.sendChannel()
|
||||
if msg.Type != "spawning" {
|
||||
t.Error("Runner should send spawning message when starting spawn, got", msg.Type)
|
||||
}
|
||||
|
||||
// spawn complete and running
|
||||
time.Sleep(2 * time.Second)
|
||||
if runner.controller.getCurrentClientsNum() != 10 {
|
||||
t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
msg = <-runner.client.sendChannel()
|
||||
if msg.Type != "spawning_complete" {
|
||||
t.Error("Runner should send spawning_complete message when spawn completed, got", msg.Type)
|
||||
}
|
||||
if runner.getState() != StateRunning {
|
||||
t.Error("State of runner is not running after spawn, got", getStateName(runner.getState()))
|
||||
}
|
||||
|
||||
// increase goroutines while running
|
||||
runner.onMessage(newMessageToWorker("rebalance", ProfileToBytes(&Profile{SpawnCount: 15, SpawnRate: 15}), nil, nil))
|
||||
runner.controller.rebalance <- true
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
if runner.getState() != StateRunning {
|
||||
t.Error("State of runner is not running after spawn, got", getStateName(runner.getState()))
|
||||
}
|
||||
if runner.controller.getCurrentClientsNum() != 15 {
|
||||
t.Error("Number of goroutines mismatches, expected: 15, current count:", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
|
||||
// stop all the workers
|
||||
runner.onMessage(newGenericMessage("stop", nil, runner.nodeID))
|
||||
if runner.getState() != StateStopped {
|
||||
t.Error("State of runner is not stopped, got", getStateName(runner.getState()))
|
||||
}
|
||||
msg = <-runner.client.sendChannel()
|
||||
if msg.Type != "client_stopped" {
|
||||
t.Error("Runner should send client_stopped message, got", msg.Type)
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
// spawn again
|
||||
runner.onMessage(newMessageToWorker("spawn", ProfileToBytes(&Profile{SpawnCount: 10, SpawnRate: 10}), nil, nil))
|
||||
go runner.start()
|
||||
|
||||
msg = <-runner.client.sendChannel()
|
||||
if msg.Type != "spawning" {
|
||||
t.Error("Runner should send spawning message when starting spawn, got", msg.Type)
|
||||
}
|
||||
|
||||
// spawn complete and running
|
||||
time.Sleep(3 * time.Second)
|
||||
if runner.controller.getCurrentClientsNum() != 10 {
|
||||
t.Error("Number of goroutines mismatches, expected: 10, current count:", runner.controller.getCurrentClientsNum())
|
||||
}
|
||||
if runner.getState() != StateRunning {
|
||||
t.Error("State of runner is not running after spawn, got", getStateName(runner.getState()))
|
||||
}
|
||||
msg = <-runner.client.sendChannel()
|
||||
if msg.Type != "spawning_complete" {
|
||||
t.Error("Runner should send spawning_complete message when spawn completed, got", msg.Type)
|
||||
}
|
||||
|
||||
// stop all the workers
|
||||
runner.onMessage(newGenericMessage("stop", nil, runner.nodeID))
|
||||
if runner.getState() != StateStopped {
|
||||
t.Error("State of runner is not stopped, got", getStateName(runner.getState()))
|
||||
}
|
||||
msg = <-runner.client.sendChannel()
|
||||
if msg.Type != "client_stopped" {
|
||||
t.Error("Runner should send client_stopped message, got", msg.Type)
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
// quit
|
||||
runner.onMessage(newGenericMessage("quit", nil, runner.nodeID))
|
||||
}
|
||||
|
||||
func TestClientListener(t *testing.T) {
|
||||
runner := newMasterRunner("localhost", 5557)
|
||||
defer runner.close()
|
||||
runner.updateState(StateInit)
|
||||
runner.setSpawnCount(10)
|
||||
runner.setSpawnRate(10)
|
||||
go runner.stateMachine()
|
||||
go runner.clientListener()
|
||||
runner.server.clients.Store("testID1", &WorkerNode{ID: "testID1", Heartbeat: 3, stream: make(chan *messager.StreamResponse, 10)})
|
||||
runner.server.clients.Store("testID2", &WorkerNode{ID: "testID2", Heartbeat: 3, stream: make(chan *messager.StreamResponse, 10)})
|
||||
runner.server.recvChannel() <- &genericMessage{
|
||||
Type: typeClientReady,
|
||||
NodeID: "testID1",
|
||||
}
|
||||
worker1, ok := runner.server.getClients().Load("testID1")
|
||||
if !ok {
|
||||
t.Fatal("error")
|
||||
}
|
||||
workerInfo1, ok := worker1.(*WorkerNode)
|
||||
if !ok {
|
||||
t.Fatal("error")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if workerInfo1.getState() != StateInit {
|
||||
t.Error("State of worker runner is not init, got", workerInfo1.getState())
|
||||
}
|
||||
runner.server.recvChannel() <- &genericMessage{
|
||||
Type: typeClientStopped,
|
||||
NodeID: "testID2",
|
||||
}
|
||||
runner.updateState(StateRunning)
|
||||
worker2, ok := runner.server.getClients().Load("testID2")
|
||||
if !ok {
|
||||
t.Fatal("error")
|
||||
}
|
||||
workerInfo2, ok := worker2.(*WorkerNode)
|
||||
if !ok {
|
||||
t.Fatal("error")
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if workerInfo2.getState() != StateStopped {
|
||||
t.Error("State of worker runner is not stopped, got", workerInfo2.getState())
|
||||
}
|
||||
runner.server.recvChannel() <- &genericMessage{
|
||||
Type: typeClientStopped,
|
||||
NodeID: "testID1",
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if runner.getState() != StateStopped {
|
||||
t.Error("State of master runner is not stopped, got", getStateName(runner.getState()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeatWorker(t *testing.T) {
|
||||
runner := newMasterRunner("localhost", 5557)
|
||||
defer runner.close()
|
||||
runner.updateState(StateInit)
|
||||
runner.setSpawnCount(10)
|
||||
runner.setSpawnRate(10)
|
||||
runner.server.clients.Store("testID1", &WorkerNode{ID: "testID1", Heartbeat: 1, State: StateInit, stream: make(chan *messager.StreamResponse, 10)})
|
||||
runner.server.clients.Store("testID2", &WorkerNode{ID: "testID2", Heartbeat: 1, State: StateInit, stream: make(chan *messager.StreamResponse, 10)})
|
||||
go runner.clientListener()
|
||||
go runner.heartbeatWorker()
|
||||
time.Sleep(4 * time.Second)
|
||||
worker1, ok := runner.server.getClients().Load("testID1")
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
workerInfo1, ok := worker1.(*WorkerNode)
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
if workerInfo1.getState() != StateMissing {
|
||||
t.Error("expected state of worker runner is missing, but got", getStateName(workerInfo1.getState()))
|
||||
}
|
||||
runner.server.recvChannel() <- &genericMessage{
|
||||
Type: typeHeartbeat,
|
||||
NodeID: "testID2",
|
||||
Data: map[string][]byte{"state": builtin.Int64ToBytes(3)},
|
||||
}
|
||||
worker2, ok := runner.server.getClients().Load("testID2")
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
workerInfo2, ok := worker2.(*WorkerNode)
|
||||
if !ok {
|
||||
t.Fatal()
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if workerInfo2.getState() == StateMissing {
|
||||
t.Error("expected state of worker runner is not missing, but got missing")
|
||||
}
|
||||
}
|
||||
577
hrp/pkg/boomer/server_grpc.go
Normal file
577
hrp/pkg/boomer/server_grpc.go
Normal file
@@ -0,0 +1,577 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/reflection"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer/data"
|
||||
"github.com/httprunner/httprunner/v4/hrp/pkg/boomer/grpc/messager"
|
||||
)
|
||||
|
||||
type WorkerNode struct {
|
||||
ID string `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
State int32 `json:"state"`
|
||||
Heartbeat int32 `json:"heartbeat"`
|
||||
UserCount int64 `json:"user_count"`
|
||||
WorkerCPUUsage float64 `json:"worker_cpu_usage"`
|
||||
CPUUsage float64 `json:"cpu_usage"`
|
||||
CPUWarningEmitted bool `json:"cpu_warning_emitted"`
|
||||
WorkerMemoryUsage float64 `json:"worker_memory_usage"`
|
||||
MemoryUsage float64 `json:"memory_usage"`
|
||||
stream chan *messager.StreamResponse
|
||||
mutex sync.RWMutex
|
||||
disconnectedChan chan bool
|
||||
}
|
||||
|
||||
func newWorkerNode(id, ip, os, arch string) *WorkerNode {
|
||||
stream := make(chan *messager.StreamResponse, 100)
|
||||
return &WorkerNode{State: StateInit, ID: id, IP: ip, OS: os, Arch: arch, Heartbeat: 3, stream: stream, disconnectedChan: make(chan bool)}
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getState() int32 {
|
||||
return atomic.LoadInt32(&w.State)
|
||||
}
|
||||
|
||||
func (w *WorkerNode) setState(state int32) {
|
||||
atomic.StoreInt32(&w.State, state)
|
||||
}
|
||||
|
||||
func (w *WorkerNode) isStarting() bool {
|
||||
return w.getState() == StateRunning || w.getState() == StateSpawning
|
||||
}
|
||||
|
||||
func (w *WorkerNode) isStopping() bool {
|
||||
return w.getState() == StateStopping
|
||||
}
|
||||
|
||||
func (w *WorkerNode) isAvailable() bool {
|
||||
state := w.getState()
|
||||
return state != StateMissing && state != StateQuitting
|
||||
}
|
||||
|
||||
func (w *WorkerNode) isReady() bool {
|
||||
state := w.getState()
|
||||
return state == StateInit || state == StateStopped
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateHeartbeat(heartbeat int32) {
|
||||
atomic.StoreInt32(&w.Heartbeat, heartbeat)
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getHeartbeat() int32 {
|
||||
return atomic.LoadInt32(&w.Heartbeat)
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateUserCount(spawnCount int64) {
|
||||
atomic.StoreInt64(&w.UserCount, spawnCount)
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getUserCount() int64 {
|
||||
return atomic.LoadInt64(&w.UserCount)
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateCPUUsage(cpuUsage float64) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
w.CPUUsage = cpuUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getCPUUsage() float64 {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return w.CPUUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateWorkerCPUUsage(workerCPUUsage float64) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
w.WorkerCPUUsage = workerCPUUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getWorkerCPUUsage() float64 {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return w.WorkerCPUUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateCPUWarningEmitted(cpuWarningEmitted bool) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
w.CPUWarningEmitted = cpuWarningEmitted
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getCPUWarningEmitted() bool {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return w.CPUWarningEmitted
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateWorkerMemoryUsage(workerMemoryUsage float64) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
w.WorkerMemoryUsage = workerMemoryUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getWorkerMemoryUsage() float64 {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return w.WorkerMemoryUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) updateMemoryUsage(memoryUsage float64) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
w.MemoryUsage = memoryUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getMemoryUsage() float64 {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return w.MemoryUsage
|
||||
}
|
||||
|
||||
func (w *WorkerNode) setStream(stream chan *messager.StreamResponse) {
|
||||
w.mutex.Lock()
|
||||
defer w.mutex.Unlock()
|
||||
w.stream = stream
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getStream() chan *messager.StreamResponse {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return w.stream
|
||||
}
|
||||
|
||||
func (w *WorkerNode) getWorkerInfo() WorkerNode {
|
||||
w.mutex.RLock()
|
||||
defer w.mutex.RUnlock()
|
||||
return WorkerNode{
|
||||
ID: w.ID,
|
||||
IP: w.IP,
|
||||
OS: w.OS,
|
||||
Arch: w.Arch,
|
||||
State: w.getState(),
|
||||
Heartbeat: w.getHeartbeat(),
|
||||
UserCount: w.getUserCount(),
|
||||
WorkerCPUUsage: w.getWorkerCPUUsage(),
|
||||
CPUUsage: w.getCPUUsage(),
|
||||
CPUWarningEmitted: w.getCPUWarningEmitted(),
|
||||
WorkerMemoryUsage: w.getWorkerMemoryUsage(),
|
||||
MemoryUsage: w.getMemoryUsage(),
|
||||
}
|
||||
}
|
||||
|
||||
type grpcServer struct {
|
||||
messager.UnimplementedMessageServer
|
||||
masterHost string
|
||||
masterPort int
|
||||
server *grpc.Server
|
||||
clients *sync.Map
|
||||
|
||||
fromWorker chan *genericMessage
|
||||
disconnectedChan chan bool
|
||||
shutdownChan chan bool
|
||||
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
var (
|
||||
errMissingMetadata = status.Errorf(codes.InvalidArgument, "missing metadata")
|
||||
errInvalidToken = status.Errorf(codes.Unauthenticated, "invalid token")
|
||||
)
|
||||
|
||||
func logger(format string, a ...interface{}) {
|
||||
// FIXME: support server-side and client-side logging to files
|
||||
log.Info().Msg(fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
// valid validates the authorization.
|
||||
func valid(authorization []string) bool {
|
||||
if len(authorization) < 1 {
|
||||
return false
|
||||
}
|
||||
token := strings.TrimPrefix(authorization[0], "Bearer ")
|
||||
return token == "httprunner-secret-token"
|
||||
}
|
||||
|
||||
func serverUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||
// authentication (token verification)
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, errMissingMetadata
|
||||
}
|
||||
if !valid(md["authorization"]) {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
m, err := handler(ctx, req)
|
||||
if err != nil {
|
||||
logger("RPC failed with error %v", err)
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
// serverWrappedStream wraps around the embedded grpc.ServerStream, and intercepts the RecvMsg and
|
||||
// SendMsg method call.
|
||||
type serverWrappedStream struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (w *serverWrappedStream) RecvMsg(m interface{}) error {
|
||||
logger("Receive a message (Type: %T) at %s", m, time.Now().Format(time.RFC3339))
|
||||
return w.ServerStream.RecvMsg(m)
|
||||
}
|
||||
|
||||
func (w *serverWrappedStream) SendMsg(m interface{}) error {
|
||||
logger("Send a message (Type: %T) at %v", m, time.Now().Format(time.RFC3339))
|
||||
return w.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func newServerWrappedStream(s grpc.ServerStream) grpc.ServerStream {
|
||||
return &serverWrappedStream{s}
|
||||
}
|
||||
|
||||
func serverStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
// authentication (token verification)
|
||||
md, ok := metadata.FromIncomingContext(ss.Context())
|
||||
if !ok {
|
||||
return errMissingMetadata
|
||||
}
|
||||
if !valid(md["authorization"]) {
|
||||
return errInvalidToken
|
||||
}
|
||||
|
||||
err := handler(srv, newServerWrappedStream(ss))
|
||||
if err != nil {
|
||||
logger("RPC failed with error %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func newServer(masterHost string, masterPort int) (server *grpcServer) {
|
||||
log.Info().Msg("Boomer is built with grpc support.")
|
||||
server = &grpcServer{
|
||||
masterHost: masterHost,
|
||||
masterPort: masterPort,
|
||||
clients: &sync.Map{},
|
||||
fromWorker: make(chan *genericMessage, 100),
|
||||
disconnectedChan: make(chan bool),
|
||||
shutdownChan: make(chan bool),
|
||||
wg: sync.WaitGroup{},
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *grpcServer) start() (err error) {
|
||||
addr := fmt.Sprintf("%v:%v", s.masterHost, s.masterPort)
|
||||
// Create tls based credential.
|
||||
creds, err := credentials.NewServerTLSFromFile(data.Path("x509/server_cert.pem"), data.Path("x509/server_key.pem"))
|
||||
if err != nil {
|
||||
log.Fatal().Msg(fmt.Sprintf("failed to load key pair: %s", err))
|
||||
}
|
||||
opts := []grpc.ServerOption{
|
||||
grpc.UnaryInterceptor(serverUnaryInterceptor),
|
||||
grpc.StreamInterceptor(serverStreamInterceptor),
|
||||
// Enable TLS for all incoming connections.
|
||||
grpc.Creds(creds),
|
||||
}
|
||||
lis, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to listen")
|
||||
return
|
||||
}
|
||||
// create gRPC server
|
||||
s.server = grpc.NewServer(opts...)
|
||||
// register message server
|
||||
messager.RegisterMessageServer(s.server, s)
|
||||
reflection.Register(s.server)
|
||||
// start grpc server
|
||||
go func() {
|
||||
err = s.server.Serve(lis)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to serve")
|
||||
return
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) Register(ctx context.Context, req *messager.RegisterRequest) (*messager.RegisterResponse, error) {
|
||||
// get client ip
|
||||
p, _ := peer.FromContext(ctx)
|
||||
clientIp := strings.Split(p.Addr.String(), ":")[0]
|
||||
// store worker information
|
||||
wn := newWorkerNode(req.NodeID, clientIp, req.Os, req.Arch)
|
||||
s.clients.Store(req.NodeID, wn)
|
||||
log.Warn().Str("worker id", req.NodeID).Msg("worker joined")
|
||||
return &messager.RegisterResponse{Code: "0", Message: "register successful"}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) SignOut(_ context.Context, req *messager.SignOutRequest) (*messager.SignOutResponse, error) {
|
||||
// delete worker information
|
||||
s.clients.Delete(req.NodeID)
|
||||
log.Warn().Str("worker id", req.NodeID).Msg("worker quited")
|
||||
return &messager.SignOutResponse{Code: "0", Message: "sign out successful"}, nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) validClientToken(token string) bool {
|
||||
_, ok := s.clients.Load(token)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *grpcServer) BidirectionalStreamingMessage(srv messager.Message_BidirectionalStreamingMessageServer) error {
|
||||
s.wg.Add(1)
|
||||
defer s.wg.Done()
|
||||
token, ok := extractToken(srv.Context())
|
||||
if !ok {
|
||||
return status.Error(codes.Unauthenticated, "missing token header")
|
||||
}
|
||||
|
||||
ok = s.validClientToken(token)
|
||||
if !ok {
|
||||
return status.Error(codes.Unauthenticated, "invalid token")
|
||||
}
|
||||
|
||||
go s.sendMsg(srv, token)
|
||||
FOR:
|
||||
for {
|
||||
select {
|
||||
case <-srv.Context().Done():
|
||||
break FOR
|
||||
case <-s.disconnectedChannel():
|
||||
break FOR
|
||||
default:
|
||||
msg, err := srv.Recv()
|
||||
if st, ok := status.FromError(err); ok {
|
||||
switch st.Code() {
|
||||
case codes.OK:
|
||||
s.fromWorker <- newGenericMessage(msg.Type, msg.Data, msg.NodeID)
|
||||
log.Info().
|
||||
Str("nodeID", msg.NodeID).
|
||||
Str("type", msg.Type).
|
||||
Interface("data", msg.Data).
|
||||
Msg("receive data from worker")
|
||||
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded:
|
||||
s.fromWorker <- newQuitMessage(token)
|
||||
break FOR
|
||||
default:
|
||||
log.Error().Err(err).Msg("failed to get stream from client")
|
||||
break FOR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Str("worker id", token).Msg("bidirectional stream closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *grpcServer) sendMsg(srv messager.Message_BidirectionalStreamingMessageServer, id string) {
|
||||
stream := s.getWorkersByID(id).getStream()
|
||||
for {
|
||||
select {
|
||||
case <-srv.Context().Done():
|
||||
return
|
||||
case <-s.disconnectedChannel():
|
||||
return
|
||||
case res := <-stream:
|
||||
if s, ok := status.FromError(srv.Send(res)); ok {
|
||||
switch s.Code() {
|
||||
case codes.OK:
|
||||
log.Info().
|
||||
Str("nodeID", res.NodeID).
|
||||
Str("type", res.Type).
|
||||
Interface("data", res.Data).
|
||||
Interface("profile", res.Profile).
|
||||
Msg("send data to worker")
|
||||
case codes.Unavailable, codes.Canceled, codes.DeadlineExceeded:
|
||||
log.Warn().Msg(fmt.Sprintf("client (%s) terminated connection", id))
|
||||
return
|
||||
default:
|
||||
log.Warn().Msg(fmt.Sprintf("failed to send to client (%s): %v", id, s.Err()))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) sendBroadcasts(msg *genericMessage) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if !workerInfo.isAvailable() {
|
||||
return true
|
||||
}
|
||||
workerInfo.getStream() <- &messager.StreamResponse{
|
||||
Type: msg.Type,
|
||||
Profile: msg.Profile,
|
||||
Data: msg.Data,
|
||||
NodeID: workerInfo.ID,
|
||||
Tasks: msg.Tasks,
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (s *grpcServer) stopServer(ctx context.Context) {
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
defer close(ch)
|
||||
// close listeners to stop accepting new connections,
|
||||
// will block on any existing transports
|
||||
s.server.GracefulStop()
|
||||
}()
|
||||
|
||||
// wait until all pending RPCs are finished
|
||||
select {
|
||||
case <-ch:
|
||||
case <-ctx.Done():
|
||||
// took too long, manually close open transports
|
||||
// e.g. watch streams
|
||||
s.server.Stop()
|
||||
|
||||
// concurrent GracefulStop should be interrupted
|
||||
<-ch
|
||||
}
|
||||
}
|
||||
|
||||
func (s *grpcServer) close() {
|
||||
// close client requests with request timeout
|
||||
timeout := 2 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
s.stopServer(ctx)
|
||||
cancel()
|
||||
|
||||
// disconnecting workers
|
||||
close(s.disconnectedChan)
|
||||
|
||||
// waiting to close bidirectional stream
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *grpcServer) recvChannel() chan *genericMessage {
|
||||
return s.fromWorker
|
||||
}
|
||||
|
||||
func (s *grpcServer) shutdownChannel() chan bool {
|
||||
return s.shutdownChan
|
||||
}
|
||||
|
||||
func (s *grpcServer) disconnectedChannel() chan bool {
|
||||
return s.disconnectedChan
|
||||
}
|
||||
|
||||
func (s *grpcServer) getWorkersByState(state int32) (wns []*WorkerNode) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.getState() == state {
|
||||
wns = append(wns, workerInfo)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return wns
|
||||
}
|
||||
|
||||
func (s *grpcServer) getWorkersByID(id string) (wn *WorkerNode) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.ID == id {
|
||||
wn = workerInfo
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return wn
|
||||
}
|
||||
|
||||
func (s *grpcServer) getWorkersLengthByState(state int32) (l int) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.getState() == state {
|
||||
l++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *grpcServer) getAllWorkers() (wns []WorkerNode) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
wns = append(wns, workerInfo.getWorkerInfo())
|
||||
}
|
||||
return true
|
||||
})
|
||||
return wns
|
||||
}
|
||||
|
||||
func (s *grpcServer) getClients() *sync.Map {
|
||||
return s.clients
|
||||
}
|
||||
|
||||
func (s *grpcServer) getAvailableClientsLength() (l int) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.isAvailable() {
|
||||
l++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *grpcServer) getReadyClientsLength() (l int) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.isReady() {
|
||||
l++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *grpcServer) getStartingClientsLength() (l int) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.isStarting() {
|
||||
l++
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *grpcServer) getCurrentUsers() (l int) {
|
||||
s.clients.Range(func(key, value interface{}) bool {
|
||||
if workerInfo, ok := value.(*WorkerNode); ok {
|
||||
if workerInfo.isStarting() {
|
||||
l += int(workerInfo.getUserCount())
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return
|
||||
}
|
||||
1
hrp/pkg/boomer/server_grpc_test.go
Normal file
1
hrp/pkg/boomer/server_grpc_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package boomer
|
||||
302
hrp/pkg/boomer/stats.go
Normal file
302
hrp/pkg/boomer/stats.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
type transaction struct {
|
||||
name string
|
||||
success bool
|
||||
elapsedTime int64
|
||||
contentSize int64
|
||||
}
|
||||
|
||||
type requestSuccess struct {
|
||||
requestType string
|
||||
name string
|
||||
responseTime int64
|
||||
responseLength int64
|
||||
}
|
||||
|
||||
type requestFailure struct {
|
||||
requestType string
|
||||
name string
|
||||
responseTime int64
|
||||
errMsg string
|
||||
}
|
||||
|
||||
type requestStats struct {
|
||||
entries map[string]*statsEntry
|
||||
errors map[string]*statsError
|
||||
total *statsEntry
|
||||
startTime int64
|
||||
|
||||
transactionChan chan *transaction
|
||||
transactionPassed int64 // accumulated number of passed transactions
|
||||
transactionFailed int64 // accumulated number of failed transactions
|
||||
|
||||
requestSuccessChan chan *requestSuccess
|
||||
requestFailureChan chan *requestFailure
|
||||
}
|
||||
|
||||
func newRequestStats() (stats *requestStats) {
|
||||
entries := make(map[string]*statsEntry)
|
||||
errors := make(map[string]*statsError)
|
||||
|
||||
stats = &requestStats{
|
||||
entries: entries,
|
||||
errors: errors,
|
||||
}
|
||||
stats.transactionChan = make(chan *transaction, 100)
|
||||
stats.requestSuccessChan = make(chan *requestSuccess, 100)
|
||||
stats.requestFailureChan = make(chan *requestFailure, 100)
|
||||
|
||||
stats.total = &statsEntry{
|
||||
Name: "Total",
|
||||
Method: "",
|
||||
}
|
||||
stats.total.reset()
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func (s *requestStats) logTransaction(name string, success bool, responseTime int64, contentLength int64) {
|
||||
if success {
|
||||
s.transactionPassed++
|
||||
} else {
|
||||
s.transactionFailed++
|
||||
s.get(name, "transaction").logFailures()
|
||||
}
|
||||
s.get(name, "transaction").log(responseTime, contentLength)
|
||||
}
|
||||
|
||||
func (s *requestStats) logRequest(method, name string, responseTime int64, contentLength int64) {
|
||||
if method != "testcase" {
|
||||
s.total.log(responseTime, contentLength)
|
||||
}
|
||||
s.get(name, method).log(responseTime, contentLength)
|
||||
}
|
||||
|
||||
func (s *requestStats) logError(method, name, err string) {
|
||||
if method != "testcase" {
|
||||
s.total.logFailures()
|
||||
}
|
||||
s.get(name, method).logFailures()
|
||||
|
||||
// store error in errors map
|
||||
key := genMD5(method, name, err)
|
||||
entry, ok := s.errors[key]
|
||||
if !ok {
|
||||
entry = &statsError{
|
||||
name: name,
|
||||
method: method,
|
||||
errMsg: err,
|
||||
}
|
||||
s.errors[key] = entry
|
||||
}
|
||||
entry.occured()
|
||||
}
|
||||
|
||||
func (s *requestStats) get(name string, method string) (entry *statsEntry) {
|
||||
entry, ok := s.entries[name+method]
|
||||
if !ok {
|
||||
newEntry := &statsEntry{
|
||||
Name: name,
|
||||
Method: method,
|
||||
ResponseTimes: make(map[int64]int64),
|
||||
}
|
||||
s.entries[name+method] = newEntry
|
||||
return newEntry
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (s *requestStats) clearAll() {
|
||||
s.total = &statsEntry{
|
||||
Name: "Total",
|
||||
Method: "",
|
||||
}
|
||||
s.total.reset()
|
||||
s.transactionPassed = 0
|
||||
s.transactionFailed = 0
|
||||
s.entries = make(map[string]*statsEntry)
|
||||
s.errors = make(map[string]*statsError)
|
||||
s.startTime = time.Now().Unix()
|
||||
}
|
||||
|
||||
func (s *requestStats) serializeStats() []interface{} {
|
||||
entries := make([]interface{}, 0, len(s.entries))
|
||||
for _, v := range s.entries {
|
||||
if !(v.NumRequests == 0 && v.NumFailures == 0) {
|
||||
entries = append(entries, v.getStrippedReport())
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *requestStats) serializeErrors() map[string]map[string]interface{} {
|
||||
errors := make(map[string]map[string]interface{})
|
||||
for k, v := range s.errors {
|
||||
errors[k] = v.toMap()
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func (s *requestStats) collectReportData() map[string]interface{} {
|
||||
data := make(map[string]interface{})
|
||||
data["transactions"] = map[string]int64{
|
||||
"passed": s.transactionPassed,
|
||||
"failed": s.transactionFailed,
|
||||
}
|
||||
data["stats"] = s.serializeStats()
|
||||
data["stats_total"] = s.total.serialize()
|
||||
data["errors"] = s.serializeErrors()
|
||||
s.errors = make(map[string]*statsError)
|
||||
return data
|
||||
}
|
||||
|
||||
// statsEntry represents a single stats entry (name and method)
|
||||
type statsEntry struct {
|
||||
// Name (URL) of this stats entry
|
||||
Name string `json:"name"`
|
||||
// Method (GET, POST, PUT, etc.)
|
||||
Method string `json:"method"`
|
||||
// The number of requests made
|
||||
NumRequests int64 `json:"num_requests"`
|
||||
// Number of failed request
|
||||
NumFailures int64 `json:"num_failures"`
|
||||
// Total sum of the response times
|
||||
TotalResponseTime int64 `json:"total_response_time"`
|
||||
// Minimum response time
|
||||
MinResponseTime int64 `json:"min_response_time"`
|
||||
// Maximum response time
|
||||
MaxResponseTime int64 `json:"max_response_time"`
|
||||
// A {response_time => count} dict that holds the response time distribution of all the requests
|
||||
// The keys (the response time in ms) are rounded to store 1, 2, ... 9, 10, 20. .. 90,
|
||||
// 100, 200 .. 900, 1000, 2000 ... 9000, in order to save memory.
|
||||
// This dict is used to calculate the median and percentile response times.
|
||||
ResponseTimes map[int64]int64 `json:"response_times"`
|
||||
// The sum of the content length of all the requests for this entry
|
||||
TotalContentLength int64 `json:"total_content_length"`
|
||||
// Time of the first request for this entry
|
||||
StartTime int64 `json:"start_time"`
|
||||
// Time of the last request for this entry
|
||||
LastRequestTimestamp int64 `json:"last_request_timestamp"`
|
||||
// Boomer doesn't allow None response time for requests like locust.
|
||||
// num_none_requests is added to keep compatible with locust.
|
||||
NumNoneRequests int64 `json:"num_none_requests"`
|
||||
}
|
||||
|
||||
func (s *statsEntry) resetStartTime() {
|
||||
atomic.StoreInt64(&s.StartTime, time.Duration(time.Now().UnixNano()).Milliseconds())
|
||||
}
|
||||
|
||||
func (s *statsEntry) reset() {
|
||||
atomic.StoreInt64(&s.StartTime, time.Duration(time.Now().UnixNano()).Milliseconds())
|
||||
s.NumRequests = 0
|
||||
s.NumFailures = 0
|
||||
s.TotalResponseTime = 0
|
||||
s.ResponseTimes = make(map[int64]int64)
|
||||
s.MinResponseTime = 0
|
||||
s.MaxResponseTime = 0
|
||||
s.LastRequestTimestamp = time.Duration(time.Now().UnixNano()).Milliseconds()
|
||||
s.TotalContentLength = 0
|
||||
}
|
||||
|
||||
func (s *statsEntry) log(responseTime int64, contentLength int64) {
|
||||
s.NumRequests++
|
||||
|
||||
s.logTimeOfRequest()
|
||||
s.logResponseTime(responseTime)
|
||||
|
||||
s.TotalContentLength += contentLength
|
||||
}
|
||||
|
||||
func (s *statsEntry) logTimeOfRequest() {
|
||||
s.LastRequestTimestamp = time.Duration(time.Now().UnixNano()).Milliseconds()
|
||||
}
|
||||
|
||||
func (s *statsEntry) logResponseTime(responseTime int64) {
|
||||
s.TotalResponseTime += responseTime
|
||||
|
||||
if s.MinResponseTime == 0 {
|
||||
s.MinResponseTime = responseTime
|
||||
}
|
||||
|
||||
if responseTime < s.MinResponseTime {
|
||||
s.MinResponseTime = responseTime
|
||||
}
|
||||
|
||||
if responseTime > s.MaxResponseTime {
|
||||
s.MaxResponseTime = responseTime
|
||||
}
|
||||
|
||||
var roundedResponseTime int64
|
||||
|
||||
// to avoid too much data that has to be transferred to the master node when
|
||||
// running in distributed mode, we save the response time rounded in a dict
|
||||
// so that 147 becomes 150, 3432 becomes 3400 and 58760 becomes 59000
|
||||
// see also locust's stats.py
|
||||
if responseTime < 100 {
|
||||
roundedResponseTime = responseTime
|
||||
} else if responseTime < 1000 {
|
||||
roundedResponseTime = int64(round(float64(responseTime), .5, -1))
|
||||
} else if responseTime < 10000 {
|
||||
roundedResponseTime = int64(round(float64(responseTime), .5, -2))
|
||||
} else {
|
||||
roundedResponseTime = int64(round(float64(responseTime), .5, -3))
|
||||
}
|
||||
|
||||
_, ok := s.ResponseTimes[roundedResponseTime]
|
||||
if !ok {
|
||||
s.ResponseTimes[roundedResponseTime] = 1
|
||||
} else {
|
||||
s.ResponseTimes[roundedResponseTime]++
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statsEntry) logFailures() {
|
||||
s.NumFailures++
|
||||
}
|
||||
|
||||
func (s *statsEntry) serialize() map[string]interface{} {
|
||||
var result map[string]interface{}
|
||||
val, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
err = json.Unmarshal(val, &result)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *statsEntry) getStrippedReport() map[string]interface{} {
|
||||
report := s.serialize()
|
||||
s.reset()
|
||||
return report
|
||||
}
|
||||
|
||||
type statsError struct {
|
||||
name string
|
||||
method string
|
||||
errMsg string
|
||||
occurrences int64
|
||||
}
|
||||
|
||||
func (err *statsError) occured() {
|
||||
err.occurrences++
|
||||
}
|
||||
|
||||
func (err *statsError) toMap() map[string]interface{} {
|
||||
m := make(map[string]interface{})
|
||||
m["method"] = err.method
|
||||
m["name"] = err.name
|
||||
m["error"] = err.errMsg
|
||||
m["occurrences"] = err.occurrences
|
||||
return m
|
||||
}
|
||||
215
hrp/pkg/boomer/stats_test.go
Normal file
215
hrp/pkg/boomer/stats_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogRequest(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 2, 30)
|
||||
newStats.logRequest("http", "success", 3, 40)
|
||||
newStats.logRequest("http", "success", 2, 40)
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
entry := newStats.get("success", "http")
|
||||
|
||||
if entry.NumRequests != 4 {
|
||||
t.Error("numRequests is wrong, expected: 4, got:", entry.NumRequests)
|
||||
}
|
||||
if entry.MinResponseTime != 1 {
|
||||
t.Error("minResponseTime is wrong, expected: 1, got:", entry.MinResponseTime)
|
||||
}
|
||||
if entry.MaxResponseTime != 3 {
|
||||
t.Error("maxResponseTime is wrong, expected: 3, got:", entry.MaxResponseTime)
|
||||
}
|
||||
if entry.TotalResponseTime != 8 {
|
||||
t.Error("totalResponseTime is wrong, expected: 8, got:", entry.TotalResponseTime)
|
||||
}
|
||||
if entry.TotalContentLength != 130 {
|
||||
t.Error("totalContentLength is wrong, expected: 130, got:", entry.TotalContentLength)
|
||||
}
|
||||
|
||||
// check newStats.total
|
||||
if newStats.total.NumRequests != 4 {
|
||||
t.Error("newStats.total.numRequests is wrong, expected: 4, got:", newStats.total.NumRequests)
|
||||
}
|
||||
if newStats.total.MinResponseTime != 1 {
|
||||
t.Error("newStats.total.minResponseTime is wrong, expected: 1, got:", newStats.total.MinResponseTime)
|
||||
}
|
||||
if newStats.total.MaxResponseTime != 3 {
|
||||
t.Error("newStats.total.maxResponseTime is wrong, expected: 3, got:", newStats.total.MaxResponseTime)
|
||||
}
|
||||
if newStats.total.TotalResponseTime != 8 {
|
||||
t.Error("newStats.total.totalResponseTime is wrong, expected: 8, got:", newStats.total.TotalResponseTime)
|
||||
}
|
||||
if newStats.total.TotalContentLength != 130 {
|
||||
t.Error("newStats.total.totalContentLength is wrong, expected: 130, got:", newStats.total.TotalContentLength)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogRequest(b *testing.B) {
|
||||
newStats := newRequestStats()
|
||||
for i := 0; i < b.N; i++ {
|
||||
newStats.logRequest("http", "success", 2, 30)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoundedResponseTime(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 147, 1)
|
||||
newStats.logRequest("http", "success", 3432, 1)
|
||||
newStats.logRequest("http", "success", 58760, 1)
|
||||
entry := newStats.get("success", "http")
|
||||
responseTimes := entry.ResponseTimes
|
||||
|
||||
if len(responseTimes) != 3 {
|
||||
t.Error("len(responseTimes) is wrong, expected: 3, got:", len(responseTimes))
|
||||
}
|
||||
|
||||
if val, ok := responseTimes[150]; !ok || val != 1 {
|
||||
t.Error("Rounded response time should be", 150)
|
||||
}
|
||||
|
||||
if val, ok := responseTimes[3400]; !ok || val != 1 {
|
||||
t.Error("Rounded response time should be", 3400)
|
||||
}
|
||||
|
||||
if val, ok := responseTimes[59000]; !ok || val != 1 {
|
||||
t.Error("Rounded response time should be", 59000)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogError(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
entry := newStats.get("failure", "http")
|
||||
|
||||
if entry.NumFailures != 3 {
|
||||
t.Error("numFailures is wrong, expected: 3, got:", entry.NumFailures)
|
||||
}
|
||||
|
||||
if newStats.total.NumFailures != 3 {
|
||||
t.Error("newStats.total.numFailures is wrong, expected: 3, got:", newStats.total.NumFailures)
|
||||
}
|
||||
|
||||
// md5("httpfailure500 error") = 547c38e4e4742c1c581f9e2809ba4f55
|
||||
err500 := newStats.errors["547c38e4e4742c1c581f9e2809ba4f55"]
|
||||
if err500.errMsg != "500 error" {
|
||||
t.Error("Error message is wrong, expected: 500 error, got:", err500.errMsg)
|
||||
}
|
||||
if err500.occurrences != 1 {
|
||||
t.Error("Error occurrences is wrong, expected: 1, got:", err500.occurrences)
|
||||
}
|
||||
|
||||
// md5("httpfailure400 error") = f391c310401ad8e10e929f2ee1a614e4
|
||||
err400 := newStats.errors["f391c310401ad8e10e929f2ee1a614e4"]
|
||||
if err400.errMsg != "400 error" {
|
||||
t.Error("Error message is wrong, expected: 400 error, got:", err400.errMsg)
|
||||
}
|
||||
if err400.occurrences != 2 {
|
||||
t.Error("Error occurrences is wrong, expected: 2, got:", err400.occurrences)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLogError(b *testing.B) {
|
||||
newStats := newRequestStats()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// LogError use md5 to calculate hash keys, it may slow down the only goroutine,
|
||||
// which consumes both requestSuccessChannel and requestFailureChannel.
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearAll(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
newStats.clearAll()
|
||||
|
||||
if newStats.total.NumRequests != 0 {
|
||||
t.Error("After clearAll(), newStats.total.numRequests is wrong, expected: 0, got:", newStats.total.NumRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearAllByChannel(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
newStats.clearAll()
|
||||
|
||||
if newStats.total.NumRequests != 0 {
|
||||
t.Error("After clearAll(), newStats.total.numRequests is wrong, expected: 0, got:", newStats.total.NumRequests)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerializeStats(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 1, 20)
|
||||
|
||||
serialized := newStats.serializeStats()
|
||||
if len(serialized) != 1 {
|
||||
t.Error("The length of serialized results is wrong, expected: 1, got:", len(serialized))
|
||||
return
|
||||
}
|
||||
|
||||
first := serialized[0]
|
||||
entry, err := deserializeStatsEntry(first)
|
||||
if err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if entry.Name != "success" {
|
||||
t.Error("The name is wrong, expected:", "success", "got:", entry.Name)
|
||||
}
|
||||
if entry.Method != "http" {
|
||||
t.Error("The method is wrong, expected:", "http", "got:", entry.Method)
|
||||
}
|
||||
if entry.NumRequests != int64(1) {
|
||||
t.Error("The num_requests is wrong, expected:", 1, "got:", entry.NumRequests)
|
||||
}
|
||||
if entry.NumFailures != int64(0) {
|
||||
t.Error("The num_failures is wrong, expected:", 0, "got:", entry.NumFailures)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSerializeErrors(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
newStats.logError("http", "failure", "400 error")
|
||||
serialized := newStats.serializeErrors()
|
||||
|
||||
if len(serialized) != 2 {
|
||||
t.Error("The length of serialized results is wrong, expected: 2, got:", len(serialized))
|
||||
return
|
||||
}
|
||||
|
||||
for key, value := range serialized {
|
||||
if key == "f391c310401ad8e10e929f2ee1a614e4" {
|
||||
err := value["error"].(string)
|
||||
if err != "400 error" {
|
||||
t.Error("expected: 400 error, got:", err)
|
||||
}
|
||||
occurrences := value["occurrences"].(int64)
|
||||
if occurrences != int64(2) {
|
||||
t.Error("expected: 2, got:", occurrences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectReportData(t *testing.T) {
|
||||
newStats := newRequestStats()
|
||||
newStats.logRequest("http", "success", 2, 30)
|
||||
newStats.logError("http", "failure", "500 error")
|
||||
result := newStats.collectReportData()
|
||||
|
||||
if _, ok := result["stats"]; !ok {
|
||||
t.Error("Key stats not found")
|
||||
}
|
||||
if _, ok := result["stats_total"]; !ok {
|
||||
t.Error("Key stats not found")
|
||||
}
|
||||
if _, ok := result["errors"]; !ok {
|
||||
t.Error("Key stats not found")
|
||||
}
|
||||
}
|
||||
13
hrp/pkg/boomer/task.go
Normal file
13
hrp/pkg/boomer/task.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package boomer
|
||||
|
||||
// Task is like the "Locust object" in locust, the python version.
|
||||
// When boomer receives a start message from master, it will spawn several goroutines to run Task.Fn.
|
||||
// But users can keep some information in the python version, they can't do the same things in boomer.
|
||||
// Because Task.Fn is a pure function.
|
||||
type Task struct {
|
||||
// The weight is used to distribute goroutines over multiple tasks.
|
||||
Weight int
|
||||
// Fn is called by the goroutines allocated to this task, in a loop.
|
||||
Fn func()
|
||||
Name string
|
||||
}
|
||||
33
hrp/pkg/boomer/ulimit.go
Normal file
33
hrp/pkg/boomer/ulimit.go
Normal file
@@ -0,0 +1,33 @@
|
||||
//go:build !windows
|
||||
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// set resource limit
|
||||
// ulimit -n 10240
|
||||
func SetUlimit(limit uint64) {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("get ulimit failed")
|
||||
return
|
||||
}
|
||||
log.Info().Uint64("limit", rLimit.Cur).Msg("get current ulimit")
|
||||
if rLimit.Cur >= limit {
|
||||
return
|
||||
}
|
||||
|
||||
rLimit.Cur = limit
|
||||
rLimit.Max = limit
|
||||
log.Info().Uint64("limit", rLimit.Cur).Msg("set current ulimit")
|
||||
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("set ulimit failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
12
hrp/pkg/boomer/ulimit_windows.go
Normal file
12
hrp/pkg/boomer/ulimit_windows.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build windows
|
||||
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// set resource limit
|
||||
func SetUlimit(limit uint64) {
|
||||
log.Warn().Msg("windows does not support setting ulimit")
|
||||
}
|
||||
150
hrp/pkg/boomer/utils.go
Normal file
150
hrp/pkg/boomer/utils.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
"github.com/shirou/gopsutil/process"
|
||||
)
|
||||
|
||||
func round(val float64, roundOn float64, places int) (newVal float64) {
|
||||
var round float64
|
||||
pow := math.Pow(10, float64(places))
|
||||
digit := pow * val
|
||||
_, div := math.Modf(digit)
|
||||
if div >= roundOn {
|
||||
round = math.Ceil(digit)
|
||||
} else {
|
||||
round = math.Floor(digit)
|
||||
}
|
||||
newVal = round / pow
|
||||
return
|
||||
}
|
||||
|
||||
// genMD5 returns the md5 hash of strings.
|
||||
func genMD5(slice ...string) string {
|
||||
h := md5.New()
|
||||
for _, v := range slice {
|
||||
io.WriteString(h, v)
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// startMemoryProfile starts memory profiling and save the results in file.
|
||||
func startMemoryProfile(file string, duration time.Duration) (err error) {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Dur("duration", duration).Msg("Start memory profiling")
|
||||
time.AfterFunc(duration, func() {
|
||||
err := pprof.WriteHeapProfile(f)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to write memory profile")
|
||||
}
|
||||
f.Close()
|
||||
log.Info().Dur("duration", duration).Msg("Stop memory profiling")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// startCPUProfile starts cpu profiling and save the results in file.
|
||||
func startCPUProfile(file string, duration time.Duration) (err error) {
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().Dur("duration", duration).Msg("Start CPU profiling")
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
time.AfterFunc(duration, func() {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
log.Info().Dur("duration", duration).Msg("Stop CPU profiling")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// generate a random nodeID like locust does, using the same algorithm.
|
||||
func getNodeID() (nodeID string) {
|
||||
hostname, _ := os.Hostname()
|
||||
id := strings.Replace(uuid.New().String(), "-", "", -1)
|
||||
nodeID = fmt.Sprintf("%s_%s", hostname, id)
|
||||
return
|
||||
}
|
||||
|
||||
// GetCurrentPidCPUUsage get current pid CPU usage
|
||||
func GetCurrentPidCPUUsage() float64 {
|
||||
currentPid := os.Getpid()
|
||||
p, err := process.NewProcess(int32(currentPid))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("failed to get CPU percent\n"))
|
||||
return 0.0
|
||||
}
|
||||
percent, err := p.CPUPercent()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("failed to get CPU percent\n"))
|
||||
return 0.0
|
||||
}
|
||||
return percent
|
||||
}
|
||||
|
||||
// GetCurrentPidCPUPercent get the percentage of current pid cpu used
|
||||
func GetCurrentPidCPUPercent() float64 {
|
||||
currentPid := os.Getpid()
|
||||
p, err := process.NewProcess(int32(currentPid))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("failed to get CPU percent\n"))
|
||||
return 0.0
|
||||
}
|
||||
percent, err := p.Percent(time.Second)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("failed to get CPU percent\n"))
|
||||
return 0.0
|
||||
}
|
||||
return percent
|
||||
}
|
||||
|
||||
// GetCurrentCPUPercent get the percentage of current cpu used
|
||||
func GetCurrentCPUPercent() float64 {
|
||||
percent, _ := cpu.Percent(time.Second, false)
|
||||
return percent[0]
|
||||
}
|
||||
|
||||
// GetCurrentMemoryPercent get the percentage of current memory used
|
||||
func GetCurrentMemoryPercent() float64 {
|
||||
memInfo, _ := mem.VirtualMemory()
|
||||
return memInfo.UsedPercent
|
||||
}
|
||||
|
||||
// GetCurrentPidMemoryUsage get current Memory usage
|
||||
func GetCurrentPidMemoryUsage() float64 {
|
||||
currentPid := os.Getpid()
|
||||
p, err := process.NewProcess(int32(currentPid))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("failed to get CPU percent\n"))
|
||||
return 0.0
|
||||
}
|
||||
percent, err := p.MemoryPercent()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg(fmt.Sprintf("failed to get CPU percent\n"))
|
||||
return 0.0
|
||||
}
|
||||
return float64(percent)
|
||||
}
|
||||
72
hrp/pkg/boomer/utils_test.go
Normal file
72
hrp/pkg/boomer/utils_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package boomer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRound(t *testing.T) {
|
||||
if int(round(float64(147.5002), .5, -1)) != 150 {
|
||||
t.Error("147.5002 should be rounded to 150")
|
||||
}
|
||||
|
||||
if int(round(float64(3432.5002), .5, -2)) != 3400 {
|
||||
t.Error("3432.5002 should be rounded to 3400")
|
||||
}
|
||||
|
||||
roundOne := round(float64(58760.5002), .5, -3)
|
||||
roundTwo := round(float64(58960.6003), .5, -3)
|
||||
if roundOne != roundTwo {
|
||||
t.Error("round(58760.5002) should be equal to round(58960.6003)")
|
||||
}
|
||||
|
||||
roundOne = round(float64(58360.5002), .5, -3)
|
||||
roundTwo = round(float64(58460.6003), .5, -3)
|
||||
if roundOne != roundTwo {
|
||||
t.Error("round(58360.5002) should be equal to round(58460.6003)")
|
||||
}
|
||||
|
||||
roundOne = round(float64(58360), .5, -3)
|
||||
roundTwo = round(float64(58460), .5, -3)
|
||||
if roundOne != roundTwo {
|
||||
t.Error("round(58360) should be equal to round(58460)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenMD5(t *testing.T) {
|
||||
hashValue := genMD5("Hello", "World!")
|
||||
if hashValue != "06e0e6637d27b2622ab52022db713ce2" {
|
||||
t.Error("Expected: 06e0e6637d27b2622ab52022db713ce2, Got: ", hashValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartMemoryProfile(t *testing.T) {
|
||||
if _, err := os.Stat("mem.pprof"); os.IsExist(err) {
|
||||
os.Remove("mem.pprof")
|
||||
}
|
||||
if err := startMemoryProfile("mem.pprof", 2*time.Second); err != nil {
|
||||
t.Error("Error starting memory profiling")
|
||||
}
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
if _, err := os.Stat("mem.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File mem.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("mem.pprof")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartCPUProfile(t *testing.T) {
|
||||
if _, err := os.Stat("cpu.pprof"); os.IsExist(err) {
|
||||
os.Remove("cpu.pprof")
|
||||
}
|
||||
if err := startCPUProfile("cpu.pprof", 2*time.Second); err != nil {
|
||||
t.Error("Error starting cpu profiling")
|
||||
}
|
||||
time.Sleep(2100 * time.Millisecond)
|
||||
if _, err := os.Stat("cpu.pprof"); os.IsNotExist(err) {
|
||||
t.Error("File cpu.pprof is not generated")
|
||||
} else {
|
||||
os.Remove("cpu.pprof")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user