Merge pull request #1432 from xucong053/bugfix

fix: state machine
This commit is contained in:
debugtalk
2022-08-15 20:51:49 +08:00
committed by GitHub
14 changed files with 378 additions and 228 deletions

View File

@@ -2,8 +2,6 @@ package hrp
import ( import (
"fmt" "fmt"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"golang.org/x/net/context"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@@ -13,9 +11,11 @@ import (
"github.com/httprunner/funplugin" "github.com/httprunner/funplugin"
"github.com/httprunner/httprunner/v4/hrp/internal/boomer" "github.com/httprunner/httprunner/v4/hrp/internal/boomer"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/json" "github.com/httprunner/httprunner/v4/hrp/internal/json"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/sdk"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/net/context"
) )
func NewStandaloneBoomer(spawnCount int64, spawnRate float64) *HRPBoomer { func NewStandaloneBoomer(spawnCount int64, spawnRate float64) *HRPBoomer {
@@ -57,7 +57,6 @@ type HRPBoomer struct {
} }
func (b *HRPBoomer) InitBoomer() { func (b *HRPBoomer) InitBoomer() {
// init output
if !b.GetProfile().DisableConsoleOutput { if !b.GetProfile().DisableConsoleOutput {
b.AddOutput(boomer.NewConsoleOutput()) b.AddOutput(boomer.NewConsoleOutput())
} }
@@ -100,6 +99,16 @@ func (b *HRPBoomer) Run(testcases ...ITestCase) {
// report execution timing event // report execution timing event
defer sdk.SendEvent(event.StartTiming("execution")) defer sdk.SendEvent(event.StartTiming("execution"))
// quit all plugins
defer func() {
pluginMap.Range(func(key, value interface{}) bool {
if plugin, ok := value.(funplugin.IPlugin); ok {
plugin.Quit()
}
return true
})
}()
taskSlice := b.ConvertTestCasesToBoomerTasks(testcases...) taskSlice := b.ConvertTestCasesToBoomerTasks(testcases...)
b.Boomer.Run(taskSlice...) b.Boomer.Run(taskSlice...)
@@ -113,15 +122,6 @@ func (b *HRPBoomer) ConvertTestCasesToBoomerTasks(testcases ...ITestCase) (taskS
os.Exit(1) os.Exit(1)
} }
// quit all plugins
defer func() {
if len(pluginMap) > 0 {
for _, plugin := range pluginMap {
plugin.Quit()
}
}
}()
for _, testcase := range testCases { for _, testcase := range testCases {
rendezvousList := initRendezvous(testcase, int64(b.GetSpawnCount())) rendezvousList := initRendezvous(testcase, int64(b.GetSpawnCount()))
task := b.convertBoomerTask(testcase, rendezvousList) task := b.convertBoomerTask(testcase, rendezvousList)
@@ -164,7 +164,7 @@ func (b *HRPBoomer) TestCasesToBytes(testcases ...ITestCase) []byte {
return testCasesBytes return testCasesBytes
} }
func (b *HRPBoomer) BytesToTestCases(testCasesBytes []byte) []*TCase { func (b *HRPBoomer) BytesToTCases(testCasesBytes []byte) []*TCase {
var testcase []*TCase var testcase []*TCase
err := json.Unmarshal(testCasesBytes, &testcase) err := json.Unmarshal(testCasesBytes, &testcase)
if err != nil { if err != nil {
@@ -177,39 +177,57 @@ func (b *HRPBoomer) Quit() {
b.Boomer.Quit() b.Boomer.Quit()
} }
func (b *HRPBoomer) runTestCases(testCases []*TCase, profile *boomer.Profile) { func (b *HRPBoomer) parseTCases(testCases []*TCase) (testcases []ITestCase) {
var testcases []ITestCase
for _, tc := range testCases { for _, tc := range testCases {
tesecase, err := tc.toTestCase()
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return
}
// create temp dir to save testcase // create temp dir to save testcase
tempDir, err := ioutil.TempDir("", "hrp_testcases") tempDir, err := ioutil.TempDir("", "hrp_testcases")
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to save testcases") log.Error().Err(err).Msg("failed to create hrp testcases directory")
return return
} }
tesecase.Config.Path = filepath.Join(tempDir, "test-case.json") if tc.Config.PluginSetting != nil {
if tesecase.Config.PluginSetting != nil { tc.Config.PluginSetting.Path = filepath.Join(tempDir, fmt.Sprintf("debugtalk.%s", tc.Config.PluginSetting.Type))
tesecase.Config.PluginSetting.Path = filepath.Join(tempDir, fmt.Sprintf("debugtalk.%s", tesecase.Config.PluginSetting.Type)) err = builtin.Bytes2File(tc.Config.PluginSetting.Content, tc.Config.PluginSetting.Path)
err = builtin.Bytes2File(tesecase.Config.PluginSetting.Content, tesecase.Config.PluginSetting.Path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to save plugin file") log.Error().Err(err).Msg("failed to save plugin file")
return return
} }
tc.Config.PluginSetting.Content = nil // remove the content in testcase
} }
err = builtin.Dump2JSON(tesecase, tesecase.Config.Path)
if tc.Config.Environs != nil {
envContent := ""
for k, v := range tc.Config.Environs {
envContent += fmt.Sprintf("%s=%s\n", k, v)
}
err = os.WriteFile(filepath.Join(tempDir, ".env"), []byte(envContent), 0o644)
if err != nil {
log.Error().Err(err).Msg("failed to dump environs")
return
}
}
tc.Config.Path = filepath.Join(tempDir, "test-case.json")
err = builtin.Dump2JSON(tc, tc.Config.Path)
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to dump testcases") log.Error().Err(err).Msg("failed to dump testcases")
return return
} }
tesecase, err := tc.toTestCase()
if err != nil {
log.Error().Err(err).Msg("failed to load testcases")
return
}
testcases = append(testcases, tesecase) testcases = append(testcases, tesecase)
} }
return testcases
}
func (b *HRPBoomer) initWorker(profile *boomer.Profile) {
// if no IP address is specified, the default IP address is that of the master
if profile.PrometheusPushgatewayURL != "" { if profile.PrometheusPushgatewayURL != "" {
urlSlice := strings.Split(profile.PrometheusPushgatewayURL, ":") urlSlice := strings.Split(profile.PrometheusPushgatewayURL, ":")
if len(urlSlice) != 2 { if len(urlSlice) != 2 {
@@ -224,16 +242,13 @@ func (b *HRPBoomer) runTestCases(testCases []*TCase, profile *boomer.Profile) {
b.SetProfile(profile) b.SetProfile(profile)
b.InitBoomer() b.InitBoomer()
log.Info().Interface("testcases", testcases).Interface("profile", profile).Msg("run tasks successful")
b.Run(testcases...)
} }
func (b *HRPBoomer) rebalanceBoomer(profile *boomer.Profile) { func (b *HRPBoomer) rebalanceRunner(profile *boomer.Profile) {
b.SetProfile(profile) b.SetSpawnCount(profile.SpawnCount)
b.SetSpawnCount(b.GetProfile().SpawnCount) b.SetSpawnRate(profile.SpawnRate)
b.SetSpawnRate(b.GetProfile().SpawnRate)
b.GetRebalanceChan() <- true b.GetRebalanceChan() <- true
log.Info().Interface("profile", profile).Msg("rebalance tasks successful") log.Info().Interface("profile", profile).Msg("rebalance tasks successfully")
} }
func (b *HRPBoomer) PollTasks(ctx context.Context) { func (b *HRPBoomer) PollTasks(ctx context.Context) {
@@ -245,11 +260,17 @@ func (b *HRPBoomer) PollTasks(ctx context.Context) {
continue continue
} }
//Todo: 过滤掉已经传输过的task //Todo: 过滤掉已经传输过的task
if task.TestCases != nil { if task.TestCasesBytes != nil {
testCases := b.BytesToTestCases(task.TestCases) // init boomer with profile
go b.runTestCases(testCases, task.Profile) b.initWorker(task.Profile)
// get testcases
testcases := b.parseTCases(b.BytesToTCases(task.TestCasesBytes))
log.Info().Interface("testcases", testcases).Interface("profile", b.GetProfile()).Msg("starting to run tasks")
// run testcases
go b.Run(testcases...)
} else { } else {
go b.rebalanceBoomer(task.Profile) // rebalance runner with profile
go b.rebalanceRunner(task.Profile)
} }
case <-b.Boomer.GetCloseChan(): case <-b.Boomer.GetCloseChan():
@@ -261,6 +282,16 @@ func (b *HRPBoomer) PollTasks(ctx context.Context) {
} }
func (b *HRPBoomer) PollTestCases(ctx context.Context) { func (b *HRPBoomer) PollTestCases(ctx context.Context) {
// quit all plugins
defer func() {
pluginMap.Range(func(key, value interface{}) bool {
if plugin, ok := value.(funplugin.IPlugin); ok {
plugin.Quit()
}
return true
})
}()
for { for {
select { select {
case <-b.Boomer.ParseTestCasesChan(): case <-b.Boomer.ParseTestCasesChan():
@@ -270,7 +301,7 @@ func (b *HRPBoomer) PollTestCases(ctx context.Context) {
tcs = append(tcs, &tcp) tcs = append(tcs, &tcp)
} }
b.TestCaseBytesChan() <- b.TestCasesToBytes(tcs...) b.TestCaseBytesChan() <- b.TestCasesToBytes(tcs...)
log.Info().Msg("put testcase successful") log.Info().Msg("put testcase successfully")
case <-b.Boomer.GetCloseChan(): case <-b.Boomer.GetCloseChan():
return return
case <-ctx.Done(): case <-ctx.Done():
@@ -379,7 +410,7 @@ func (b *HRPBoomer) convertBoomerTask(testcase *TestCase, rendezvousList []*Rend
// transaction // transaction
// FIXME: support nested transactions // FIXME: support nested transactions
if step.Struct().Transaction.Type == transactionEnd { // only record when transaction ends if step.Struct().Transaction.Type == transactionEnd { // only record when transaction ends
b.RecordTransaction(stepResult.Name, transactionSuccess, stepResult.Elapsed, 0) b.RecordTransaction(step.Struct().Transaction.Name, transactionSuccess, stepResult.Elapsed, 0)
transactionSuccess = true // reset flag for next transaction transactionSuccess = true // reset flag for next transaction
} }
} else if stepResult.StepType == stepTypeRendezvous { } else if stepResult.StepType == stepTypeRendezvous {

View File

@@ -63,6 +63,18 @@ type Profile struct {
DisableKeepalive bool `json:"disable-keepalive,omitempty" yaml:"disable-keepalive,omitempty" mapstructure:"disable-keepalive,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 { func (b *Boomer) GetProfile() *Profile {
switch b.mode { switch b.mode {
case DistributedMasterMode: case DistributedMasterMode:
@@ -158,7 +170,18 @@ func (b *Boomer) RunWorker() {
// TestCaseBytesChan gets test case bytes chan // TestCaseBytesChan gets test case bytes chan
func (b *Boomer) TestCaseBytesChan() chan []byte { func (b *Boomer) TestCaseBytesChan() chan []byte {
return b.masterRunner.testCaseBytes 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 { func ProfileToBytes(profile *Profile) []byte {
@@ -192,7 +215,7 @@ func (b *Boomer) GetTasksChan() chan *task {
func (b *Boomer) GetRebalanceChan() chan bool { func (b *Boomer) GetRebalanceChan() chan bool {
switch b.mode { switch b.mode {
case DistributedWorkerMode: case DistributedWorkerMode:
return b.workerRunner.rebalance return b.workerRunner.controller.getRebalanceChan()
default: default:
return nil return nil
} }
@@ -469,6 +492,9 @@ func (b *Boomer) Start(Args *Profile) error {
if b.masterRunner.isStopping() { if b.masterRunner.isStopping() {
return errors.New("Please wait for all workers to finish") 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.SetSpawnCount(Args.SpawnCount)
b.SetSpawnRate(Args.SpawnRate) b.SetSpawnRate(Args.SpawnRate)
b.SetProfile(Args) b.SetProfile(Args)
@@ -481,6 +507,9 @@ func (b *Boomer) ReBalance(Args *Profile) error {
if !b.masterRunner.isStarting() { if !b.masterRunner.isStarting() {
return errors.New("no start") 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.SetSpawnCount(Args.SpawnCount)
b.SetSpawnRate(Args.SpawnRate) b.SetSpawnRate(Args.SpawnRate)
b.SetProfile(Args) b.SetProfile(Args)
@@ -505,8 +534,9 @@ func (b *Boomer) GetWorkersInfo() []WorkerNode {
func (b *Boomer) GetMasterInfo() map[string]interface{} { func (b *Boomer) GetMasterInfo() map[string]interface{} {
masterInfo := make(map[string]interface{}) masterInfo := make(map[string]interface{})
masterInfo["state"] = b.masterRunner.getState() masterInfo["state"] = b.masterRunner.getState()
masterInfo["workers"] = b.masterRunner.server.getClientsLength() masterInfo["workers"] = b.masterRunner.server.getAvailableClientsLength()
masterInfo["target_users"] = b.masterRunner.getSpawnCount() masterInfo["target_users"] = b.masterRunner.getSpawnCount()
masterInfo["current_users"] = b.masterRunner.server.getCurrentUsers()
return masterInfo return masterInfo
} }

View File

@@ -313,11 +313,12 @@ func (c *grpcClient) sendMessage(msg *genericMessage) {
return return
} }
err := c.config.getBiStreamClient().Send(&messager.StreamRequest{Type: msg.Type, Data: msg.Data, NodeID: msg.NodeID}) err := c.config.getBiStreamClient().Send(&messager.StreamRequest{Type: msg.Type, Data: msg.Data, NodeID: msg.NodeID})
switch err { if err == nil {
case nil:
atomic.StoreInt32(&c.failCount, 0) atomic.StoreInt32(&c.failCount, 0)
default: return
//log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message") }
//log.Error().Err(err).Interface("genericMessage", *msg).Msg("failed to send message")
if msg.Type == "heartbeat" {
atomic.AddInt32(&c.failCount, 1) atomic.AddInt32(&c.failCount, 1)
} }
} }

View File

@@ -19,8 +19,8 @@ type genericMessage struct {
} }
type task struct { type task struct {
Profile *Profile `json:"profile,omitempty"` Profile *Profile `json:"profile,omitempty"`
TestCases []byte `json:"testcases,omitempty"` TestCasesBytes []byte `json:"testcases,omitempty"`
} }
func newGenericMessage(t string, data map[string][]byte, nodeID string) (msg *genericMessage) { func newGenericMessage(t string, data map[string][]byte, nodeID string) (msg *genericMessage) {

View File

@@ -6,6 +6,7 @@ import (
"os" "os"
"sort" "sort"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
@@ -454,8 +455,8 @@ var (
) )
var ( var (
minResponseTimeMap = map[string]float64{} minResponseTimeMap = sync.Map{}
maxResponseTimeMap = map[string]float64{} maxResponseTimeMap = sync.Map{}
) )
// NewPrometheusPusherOutput returns a PrometheusPusherOutput. // NewPrometheusPusherOutput returns a PrometheusPusherOutput.
@@ -577,19 +578,19 @@ func (o *PrometheusPusherOutput) OnEvent(data map[string]interface{}) {
} }
// every stat in total // every stat in total
key := fmt.Sprintf("%v_%v", method, name) key := fmt.Sprintf("%v_%v", method, name)
if _, ok := minResponseTimeMap[key]; !ok { minResponseTime, loaded := minResponseTimeMap.LoadOrStore(key, float64(stat.MinResponseTime))
minResponseTimeMap[key] = float64(stat.MinResponseTime) if loaded {
} else { minResponseTime = math.Min(minResponseTime.(float64), float64(stat.MinResponseTime))
minResponseTimeMap[key] = math.Min(float64(stat.MinResponseTime), minResponseTimeMap[key]) minResponseTimeMap.Store(key, minResponseTime)
} }
gaugeTotalMinResponseTime.WithLabelValues(method, name).Set(minResponseTimeMap[key]) gaugeTotalMinResponseTime.WithLabelValues(method, name).Set(minResponseTime.(float64))
if _, ok := maxResponseTimeMap[key]; !ok { maxResponseTime, loaded := maxResponseTimeMap.LoadOrStore(key, float64(stat.MaxResponseTime))
maxResponseTimeMap[key] = float64(stat.MaxResponseTime) if loaded {
} else { maxResponseTime = math.Max(maxResponseTime.(float64), float64(stat.MaxResponseTime))
maxResponseTimeMap[key] = math.Max(float64(stat.MaxResponseTime), maxResponseTimeMap[key]) maxResponseTimeMap.Store(key, maxResponseTime)
} }
gaugeTotalMaxResponseTime.WithLabelValues(method, name).Set(maxResponseTimeMap[key]) gaugeTotalMaxResponseTime.WithLabelValues(method, name).Set(maxResponseTime.(float64))
counterTotalNumRequests.WithLabelValues(method, name).Add(float64(stat.NumRequests)) counterTotalNumRequests.WithLabelValues(method, name).Add(float64(stat.NumRequests))
counterTotalNumFailures.WithLabelValues(method, name).Add(float64(stat.NumFailures)) counterTotalNumFailures.WithLabelValues(method, name).Add(float64(stat.NumFailures))
@@ -639,4 +640,7 @@ func resetPrometheusMetrics() {
gaugeTotalFailPerSec.Set(0) gaugeTotalFailPerSec.Set(0)
gaugeTransactionsPassed.Set(0) gaugeTransactionsPassed.Set(0)
gaugeTransactionsFailed.Set(0) gaugeTransactionsFailed.Set(0)
minResponseTimeMap = sync.Map{}
maxResponseTimeMap = sync.Map{}
} }

View File

@@ -49,10 +49,10 @@ func getStateName(state int32) (stateName string) {
} }
const ( const (
reportStatsInterval = 3 * time.Second reportStatsInterval = 3 * time.Second
heartbeatInterval = 1 * time.Second heartbeatInterval = 1 * time.Second
heartbeatLiveness = 3 * time.Second heartbeatLiveness = 3 * time.Second
reconnectInterval = 3 * time.Second stateMachineInterval = 1 * time.Second
) )
type Loop struct { type Loop struct {
@@ -86,6 +86,7 @@ type Controller struct {
currentClientsNum int64 // current clients count currentClientsNum int64 // current clients count
spawnCount int64 // target clients to spawn spawnCount int64 // target clients to spawn
spawnRate float64 spawnRate float64
rebalance chan bool // dynamically balance boomer running parameters
spawnDone chan struct{} spawnDone chan struct{}
tasks []*Task tasks []*Task
} }
@@ -143,6 +144,12 @@ func (c *Controller) spawnCompete() {
close(c.spawnDone) close(c.spawnDone)
} }
func (c *Controller) getRebalanceChan() chan bool {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.rebalance
}
func (c *Controller) isFinished() bool { func (c *Controller) isFinished() bool {
// return true when workers acquired // return true when workers acquired
return atomic.LoadInt64(&c.currentClientsNum) == atomic.LoadInt64(&c.spawnCount) return atomic.LoadInt64(&c.currentClientsNum) == atomic.LoadInt64(&c.spawnCount)
@@ -178,6 +185,7 @@ func (c *Controller) reset() {
c.spawnRate = 0 c.spawnRate = 0
atomic.StoreInt64(&c.currentClientsNum, 0) atomic.StoreInt64(&c.currentClientsNum, 0)
c.spawnDone = make(chan struct{}) c.spawnDone = make(chan struct{})
c.rebalance = make(chan bool)
c.tasks = []*Task{} c.tasks = []*Task{}
c.once = sync.Once{} c.once = sync.Once{}
} }
@@ -199,9 +207,6 @@ type runner struct {
controller *Controller controller *Controller
loop *Loop // specify loop count for testcase, count = loopCount * spawnCount loop *Loop // specify loop count for testcase, count = loopCount * spawnCount
// dynamically balance boomer running parameters
rebalance chan bool
// stop signals the run goroutine should shutdown. // stop signals the run goroutine should shutdown.
stopChan chan bool stopChan chan bool
// all running workers(goroutines) will select on this channel. // all running workers(goroutines) will select on this channel.
@@ -358,7 +363,6 @@ func (r *runner) reportTestResult() {
func (r *runner) reset() { func (r *runner) reset() {
r.controller.reset() r.controller.reset()
r.stats.clearAll() r.stats.clearAll()
r.rebalance = make(chan bool)
r.stoppingChan = make(chan bool) r.stoppingChan = make(chan bool)
r.doneChan = make(chan bool) r.doneChan = make(chan bool)
r.reportedChan = make(chan bool) r.reportedChan = make(chan bool)
@@ -430,16 +434,18 @@ func (r *runner) spawnWorkers(spawnCount int64, spawnRate float64, quit chan boo
continue continue
} }
r.controller.once.Do(func() { r.controller.once.Do(
// spawning compete func() {
r.controller.spawnCompete() // spawning compete
if spawnCompleteFunc != nil { r.controller.spawnCompete()
spawnCompleteFunc() if spawnCompleteFunc != nil {
} spawnCompleteFunc()
r.updateState(StateRunning) }
}) r.updateState(StateRunning)
},
)
<-r.rebalance <-r.controller.getRebalanceChan()
if r.isStarting() { if r.isStarting() {
// rebalance spawn count // rebalance spawn count
r.controller.setSpawn(r.getSpawnCount(), r.getSpawnRate()) r.controller.setSpawn(r.getSpawnCount(), r.getSpawnRate())
@@ -629,7 +635,7 @@ func (r *localRunner) start() {
r.wgMu.Lock() r.wgMu.Lock()
r.updateState(StateStopping) r.updateState(StateStopping)
close(r.stoppingChan) close(r.stoppingChan)
close(r.rebalance) close(r.controller.rebalance)
r.wgMu.Unlock() r.wgMu.Unlock()
// wait for goroutines before closing // wait for goroutines before closing
@@ -668,7 +674,8 @@ type workerRunner struct {
masterPort int masterPort int
client *grpcClient client *grpcClient
profile *Profile profile *Profile
testCasesBytes []byte
tasksChan chan *task tasksChan chan *task
@@ -714,10 +721,10 @@ func (r *workerRunner) onSpawnMessage(msg *genericMessage) {
log.Error().Msg("miss tasks") log.Error().Msg("miss tasks")
} }
r.tasksChan <- &task{ r.tasksChan <- &task{
Profile: profile, Profile: profile,
TestCases: msg.Tasks, TestCasesBytes: msg.Tasks,
} }
log.Info().Msg("on spawn message successful") log.Info().Msg("on spawn message successfully")
} }
func (r *workerRunner) onRebalanceMessage(msg *genericMessage) { func (r *workerRunner) onRebalanceMessage(msg *genericMessage) {
@@ -731,7 +738,7 @@ func (r *workerRunner) onRebalanceMessage(msg *genericMessage) {
r.tasksChan <- &task{ r.tasksChan <- &task{
Profile: profile, Profile: profile,
} }
log.Info().Msg("on rebalance message successful") log.Info().Msg("on rebalance message successfully")
} }
// Runner acts as a state machine. // Runner acts as a state machine.
@@ -831,6 +838,9 @@ func (r *workerRunner) run() {
// wait for goroutines before closing // wait for goroutines before closing
r.wg.Wait() r.wg.Wait()
// notify master that worker is quitting
r.onQuiting()
var ticker = time.NewTicker(1 * time.Second) var ticker = time.NewTicker(1 * time.Second)
if r.client != nil { if r.client != nil {
// waitting for quit message is sent to master // waitting for quit message is sent to master
@@ -879,6 +889,7 @@ func (r *workerRunner) run() {
if !r.isStarting() && !r.isStopping() { if !r.isStarting() && !r.isStopping() {
r.updateState(StateMissing) r.updateState(StateMissing)
} }
continue
} }
CPUUsage := GetCurrentCPUPercent() CPUUsage := GetCurrentCPUPercent()
MemoryUsage := GetCurrentMemoryPercent() MemoryUsage := GetCurrentMemoryPercent()
@@ -918,13 +929,18 @@ func (r *workerRunner) start() {
// block concurrent waitgroup adds in GoAttach while stopping // block concurrent waitgroup adds in GoAttach while stopping
r.wgMu.Lock() r.wgMu.Lock()
r.updateState(StateStopping) r.updateState(StateStopping)
close(r.controller.rebalance)
close(r.stoppingChan) close(r.stoppingChan)
close(r.rebalance)
r.wgMu.Unlock() r.wgMu.Unlock()
// wait for goroutines before closing // wait for goroutines before closing
r.wg.Wait() r.wg.Wait()
// reset loop
if r.loop != nil {
r.loop = nil
}
close(r.doneChan) close(r.doneChan)
// wait until all stats are reported successfully // wait until all stats are reported successfully
@@ -951,7 +967,6 @@ func (r *workerRunner) stop() {
} }
func (r *workerRunner) close() { func (r *workerRunner) close() {
r.onQuiting()
close(r.closeChan) close(r.closeChan)
} }
@@ -970,8 +985,8 @@ type masterRunner struct {
profile *Profile profile *Profile
parseTestCasesChan chan bool parseTestCasesChan chan bool
testCaseBytes chan []byte testCaseBytesChan chan []byte
tcb []byte testCasesBytes []byte
} }
func newMasterRunner(masterBindHost string, masterBindPort int) *masterRunner { func newMasterRunner(masterBindHost string, masterBindPort int) *masterRunner {
@@ -988,7 +1003,7 @@ func newMasterRunner(masterBindHost string, masterBindPort int) *masterRunner {
masterBindPort: masterBindPort, masterBindPort: masterBindPort,
server: newServer(masterBindHost, masterBindPort), server: newServer(masterBindHost, masterBindPort),
parseTestCasesChan: make(chan bool), parseTestCasesChan: make(chan bool),
testCaseBytes: make(chan []byte), testCaseBytesChan: make(chan []byte),
} }
} }
@@ -1011,23 +1026,15 @@ func (r *masterRunner) heartbeatWorker() {
if !ok { if !ok {
log.Error().Msg("failed to get worker information") log.Error().Msg("failed to get worker information")
} }
if atomic.LoadInt32(&workerInfo.Heartbeat) < 0 { go func() {
if workerInfo.getState() == StateQuitting { if atomic.LoadInt32(&workerInfo.Heartbeat) < 0 {
return true if workerInfo.getState() != StateMissing {
} workerInfo.setState(StateMissing)
if workerInfo.getState() != StateMissing {
workerInfo.setState(StateMissing)
}
if r.isStopping() {
// all running workers missed, setting state to stopped
if r.server.getClientsLength() <= 0 {
r.updateState(StateStopped)
} }
return true } else {
atomic.AddInt32(&workerInfo.Heartbeat, -1)
} }
} else { }()
atomic.AddInt32(&workerInfo.Heartbeat, -1)
}
return true return true
}) })
case <-reportTicker.C: case <-reportTicker.C:
@@ -1051,72 +1058,85 @@ func (r *masterRunner) clientListener() {
if !ok { if !ok {
continue continue
} }
switch msg.Type { go func() {
case typeClientReady: switch msg.Type {
workerInfo.setState(StateInit) case typeClientReady:
if r.getState() == StateRunning { workerInfo.setState(StateInit)
log.Warn().Str("worker id", workerInfo.ID).Msg("worker joined, ready to rebalance the load of each worker") case typeClientStopped:
workerInfo.setState(StateStopped)
case typeHeartbeat:
if workerInfo.getState() == StateMissing {
workerInfo.setState(int32(builtin.BytesToInt64(msg.Data["state"])))
}
workerInfo.updateHeartbeat(3)
currentCPUUsage, ok := msg.Data["current_cpu_usage"]
if ok {
workerInfo.updateCPUUsage(builtin.ByteToFloat64(currentCPUUsage))
}
currentPidCpuUsage, ok := msg.Data["current_pid_cpu_usage"]
if ok {
workerInfo.updateWorkerCPUUsage(builtin.ByteToFloat64(currentPidCpuUsage))
}
currentMemoryUsage, ok := msg.Data["current_memory_usage"]
if ok {
workerInfo.updateMemoryUsage(builtin.ByteToFloat64(currentMemoryUsage))
}
currentPidMemoryUsage, ok := msg.Data["current_pid_memory_usage"]
if ok {
workerInfo.updateWorkerMemoryUsage(builtin.ByteToFloat64(currentPidMemoryUsage))
}
currentUsers, ok := msg.Data["current_users"]
if ok {
workerInfo.updateUserCount(builtin.BytesToInt64(currentUsers))
}
case typeSpawning:
workerInfo.setState(StateSpawning)
case typeSpawningComplete:
workerInfo.setState(StateRunning)
case typeQuit:
if workerInfo.getState() == StateQuitting {
break
}
workerInfo.setState(StateQuitting)
case typeException:
// Todo
default:
}
}()
}
}
}
func (r *masterRunner) stateMachine() {
ticker := time.NewTicker(stateMachineInterval)
for {
select {
case <-r.closeChan:
return
case <-ticker.C:
switch r.getState() {
case StateSpawning:
if r.server.getCurrentUsers() == int(r.getSpawnCount()) {
log.Warn().Msg("all workers spawn done, setting state as running")
r.updateState(StateRunning)
}
case StateRunning:
if r.server.getStartingClientsLength() == 0 {
r.updateState(StateStopped)
continue
}
if r.server.getWorkersLengthByState(StateInit) != 0 {
err := r.rebalance() err := r.rebalance()
if err != nil { if err != nil {
log.Error().Err(err).Msg("failed to rebalance") log.Error().Err(err).Msg("failed to rebalance")
} }
} }
case typeClientStopped: case StateStopping:
workerInfo.setState(StateStopped) if r.server.getReadyClientsLength() == r.server.getAvailableClientsLength() {
if r.server.getWorkersLengthByState(StateStopped)+r.server.getWorkersLengthByState(StateInit) == r.server.getClientsLength() {
r.updateState(StateStopped) r.updateState(StateStopped)
} }
case typeHeartbeat:
if workerInfo.getState() != int32(builtin.BytesToInt64(msg.Data["state"])) {
workerInfo.setState(int32(builtin.BytesToInt64(msg.Data["state"])))
}
workerInfo.updateHeartbeat(3)
currentCPUUsage, ok := msg.Data["current_cpu_usage"]
if ok {
workerInfo.updateCPUUsage(builtin.ByteToFloat64(currentCPUUsage))
}
currentPidCpuUsage, ok := msg.Data["current_pid_cpu_usage"]
if ok {
workerInfo.updateWorkerCPUUsage(builtin.ByteToFloat64(currentPidCpuUsage))
}
currentMemoryUsage, ok := msg.Data["current_memory_usage"]
if ok {
workerInfo.updateMemoryUsage(builtin.ByteToFloat64(currentMemoryUsage))
}
currentPidMemoryUsage, ok := msg.Data["current_pid_memory_usage"]
if ok {
workerInfo.updateWorkerMemoryUsage(builtin.ByteToFloat64(currentPidMemoryUsage))
}
currentUsers, ok := msg.Data["current_users"]
if ok {
workerInfo.updateUserCount(builtin.BytesToInt64(currentUsers))
}
case typeSpawning:
workerInfo.setState(StateSpawning)
case typeSpawningComplete:
workerInfo.setState(StateRunning)
if r.server.getWorkersLengthByState(StateRunning) == r.server.getClientsLength() {
log.Warn().Msg("all workers spawn done, setting state as running")
r.updateState(StateRunning)
}
case typeQuit:
if workerInfo.getState() == StateQuitting {
break
}
workerInfo.setState(StateQuitting)
if r.isStarting() {
if r.server.getClientsLength() > 0 {
log.Warn().Str("worker id", workerInfo.ID).Msg("worker quited, ready to rebalance the load of each worker")
err := r.rebalance()
if err != nil {
log.Error().Err(err).Msg("failed to rebalance")
}
}
}
case typeException:
// Todo
default:
} }
} }
} }
} }
@@ -1146,7 +1166,7 @@ func (r *masterRunner) run() {
case <-r.closeChan: case <-r.closeChan:
return return
case <-ticker.C: case <-ticker.C:
c := r.server.getClientsLength() c := r.server.getAvailableClientsLength()
log.Info().Msg(fmt.Sprintf("expected worker number: %v, current worker count: %v", r.expectWorkers, c)) log.Info().Msg(fmt.Sprintf("expected worker number: %v, current worker count: %v", r.expectWorkers, c))
if c >= r.expectWorkers { if c >= r.expectWorkers {
err = r.start() err = r.start()
@@ -1165,6 +1185,9 @@ func (r *masterRunner) run() {
}() }()
} }
// master state machine
r.goAttach(r.stateMachine)
// listen and deal message from worker // listen and deal message from worker
r.goAttach(r.clientListener) r.goAttach(r.clientListener)
@@ -1174,13 +1197,13 @@ func (r *masterRunner) run() {
} }
func (r *masterRunner) start() error { func (r *masterRunner) start() error {
numWorkers := r.server.getClientsLength() numWorkers := r.server.getAvailableClientsLength()
if numWorkers == 0 { if numWorkers == 0 {
return errors.New("current available workers: 0") return errors.New("current available workers: 0")
} }
// fetching testcase // fetching testcases
testcase, err := r.fetchTestCase() testCasesBytes, err := r.fetchTestCases()
if err != nil { if err != nil {
return err return err
} }
@@ -1223,19 +1246,19 @@ func (r *masterRunner) start() error {
Type: "spawn", Type: "spawn",
Profile: ProfileToBytes(workerProfile), Profile: ProfileToBytes(workerProfile),
NodeID: workerInfo.ID, NodeID: workerInfo.ID,
Tasks: testcase, Tasks: testCasesBytes,
} }
cur++ cur++
} }
return true return true
}) })
log.Warn().Interface("profile", r.profile).Msg("send spawn data to worker successful") log.Warn().Interface("profile", r.profile).Msg("send spawn data to worker successfully")
return nil return nil
} }
func (r *masterRunner) rebalance() error { func (r *masterRunner) rebalance() error {
numWorkers := r.server.getClientsLength() numWorkers := r.server.getAvailableClientsLength()
if numWorkers == 0 { if numWorkers == 0 {
return errors.New("current available workers: 0") return errors.New("current available workers: 0")
} }
@@ -1276,7 +1299,7 @@ func (r *masterRunner) rebalance() error {
Type: "spawn", Type: "spawn",
Profile: ProfileToBytes(workerProfile), Profile: ProfileToBytes(workerProfile),
NodeID: workerInfo.ID, NodeID: workerInfo.ID,
Tasks: r.tcb, Tasks: r.testCasesBytes,
} }
} else { } else {
workerInfo.getStream() <- &messager.StreamResponse{ workerInfo.getStream() <- &messager.StreamResponse{
@@ -1290,22 +1313,22 @@ func (r *masterRunner) rebalance() error {
return true return true
}) })
log.Warn().Msg("send rebalance data to worker successful") log.Warn().Msg("send rebalance data to worker successfully")
return nil return nil
} }
func (r *masterRunner) fetchTestCase() ([]byte, error) { func (r *masterRunner) fetchTestCases() ([]byte, error) {
ticker := time.NewTicker(30 * time.Second) ticker := time.NewTicker(30 * time.Second)
if len(r.testCaseBytes) > 0 { if len(r.testCaseBytesChan) > 0 {
<-r.testCaseBytes <-r.testCaseBytesChan
} }
r.parseTestCasesChan <- true r.parseTestCasesChan <- true
select { select {
case <-ticker.C: case <-ticker.C:
return nil, errors.New("parse testcases timeout") return nil, errors.New("parse testcases timeout")
case tcb := <-r.testCaseBytes: case testCasesBytes := <-r.testCaseBytesChan:
r.tcb = tcb r.testCasesBytes = testCasesBytes
return tcb, nil return testCasesBytes, nil
} }
} }
@@ -1336,22 +1359,23 @@ func (r *masterRunner) close() {
func (r *masterRunner) reportStats() { func (r *masterRunner) reportStats() {
currentTime := time.Now() currentTime := time.Now()
println() println()
println("========================= HttpRunner Master for Distributed Load Testing ========================= ") println("==================================== HttpRunner Master for Distributed Load Testing ==================================== ")
println(fmt.Sprintf("Current time: %s, State: %v, Current Available Workers: %v, Target Users: %v", println(fmt.Sprintf("Current time: %s, State: %v, Current Available Workers: %v, Target Users: %v, Current Users: %v",
currentTime.Format("2006/01/02 15:04:05"), getStateName(r.getState()), r.server.getClientsLength(), r.getSpawnCount())) currentTime.Format("2006/01/02 15:04:05"), getStateName(r.getState()), r.server.getAvailableClientsLength(), r.getSpawnCount(), r.server.getCurrentUsers()))
table := tablewriter.NewWriter(os.Stdout) table := tablewriter.NewWriter(os.Stdout)
table.SetColMinWidth(0, 20) table.SetColMinWidth(0, 40)
table.SetColMinWidth(1, 10) table.SetColMinWidth(1, 10)
table.SetColMinWidth(2, 10)
table.SetHeader([]string{"Worker ID", "IP", "State", "Current Users", "CPU Usage (%)", "Memory Usage (%)"}) table.SetHeader([]string{"Worker ID", "IP", "State", "Current Users", "CPU Usage (%)", "Memory Usage (%)"})
for _, worker := range r.server.getAllWorkers() { for _, worker := range r.server.getAllWorkers() {
row := make([]string, 6) row := make([]string, 6)
row[0] = worker.ID row[0] = worker.ID
row[1] = worker.IP row[1] = worker.IP
row[2] = fmt.Sprintf("%v", getStateName(worker.getState())) row[2] = fmt.Sprintf("%v", getStateName(worker.State))
row[3] = fmt.Sprintf("%v", worker.getUserCount()) row[3] = fmt.Sprintf("%v", worker.UserCount)
row[4] = fmt.Sprintf("%.2f", worker.getCPUUsage()) row[4] = fmt.Sprintf("%.2f", worker.CPUUsage)
row[5] = fmt.Sprintf("%.2f", worker.getMemoryUsage()) row[5] = fmt.Sprintf("%.2f", worker.MemoryUsage)
table.Append(row) table.Append(row)
} }
table.Render() table.Render()

View File

@@ -338,6 +338,7 @@ func TestOnQuitMessage(t *testing.T) {
go runner.onMessage(newGenericMessage("quit", nil, runner.nodeID)) go runner.onMessage(newGenericMessage("quit", nil, runner.nodeID))
close(runner.doneChan) close(runner.doneChan)
<-runner.closeChan <-runner.closeChan
runner.onQuiting()
if runner.getState() != StateQuitting { if runner.getState() != StateQuitting {
t.Error("Runner's state should be StateQuitting") t.Error("Runner's state should be StateQuitting")
} }
@@ -348,6 +349,7 @@ func TestOnQuitMessage(t *testing.T) {
runner.client.shutdownChan = make(chan bool) runner.client.shutdownChan = make(chan bool)
runner.onMessage(newGenericMessage("quit", nil, runner.nodeID)) runner.onMessage(newGenericMessage("quit", nil, runner.nodeID))
<-runner.closeChan <-runner.closeChan
runner.onQuiting()
if runner.getState() != StateQuitting { if runner.getState() != StateQuitting {
t.Error("Runner's state should be StateQuitting") t.Error("Runner's state should be StateQuitting")
} }
@@ -395,7 +397,7 @@ func TestOnMessage(t *testing.T) {
// increase goroutines while running // increase goroutines while running
runner.onMessage(newMessageToWorker("rebalance", ProfileToBytes(&Profile{SpawnCount: 15, SpawnRate: 15}), nil, nil)) runner.onMessage(newMessageToWorker("rebalance", ProfileToBytes(&Profile{SpawnCount: 15, SpawnRate: 15}), nil, nil))
runner.rebalance <- true runner.controller.rebalance <- true
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
if runner.getState() != StateRunning { if runner.getState() != StateRunning {
@@ -460,6 +462,7 @@ func TestClientListener(t *testing.T) {
runner.updateState(StateInit) runner.updateState(StateInit)
runner.setSpawnCount(10) runner.setSpawnCount(10)
runner.setSpawnRate(10) runner.setSpawnRate(10)
go runner.stateMachine()
go runner.clientListener() go runner.clientListener()
runner.server.clients.Store("testID1", &WorkerNode{ID: "testID1", Heartbeat: 3, stream: make(chan *messager.StreamResponse, 10)}) 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.clients.Store("testID2", &WorkerNode{ID: "testID2", Heartbeat: 3, stream: make(chan *messager.StreamResponse, 10)})
@@ -483,6 +486,7 @@ func TestClientListener(t *testing.T) {
Type: typeClientStopped, Type: typeClientStopped,
NodeID: "testID2", NodeID: "testID2",
} }
runner.updateState(StateRunning)
worker2, ok := runner.server.getClients().Load("testID2") worker2, ok := runner.server.getClients().Load("testID2")
if !ok { if !ok {
t.Fatal("error") t.Fatal("error")
@@ -515,7 +519,7 @@ func TestHeartbeatWorker(t *testing.T) {
runner.server.clients.Store("testID2", &WorkerNode{ID: "testID2", 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.clientListener()
go runner.heartbeatWorker() go runner.heartbeatWorker()
time.Sleep(3 * time.Second) time.Sleep(4 * time.Second)
worker1, ok := runner.server.getClients().Load("testID1") worker1, ok := runner.server.getClients().Load("testID1")
if !ok { if !ok {
t.Fatal() t.Fatal()
@@ -525,7 +529,7 @@ func TestHeartbeatWorker(t *testing.T) {
t.Fatal() t.Fatal()
} }
if workerInfo1.getState() != StateMissing { if workerInfo1.getState() != StateMissing {
t.Error("expected state of worker runner is missing, but got", workerInfo1.getState()) t.Error("expected state of worker runner is missing, but got", getStateName(workerInfo1.getState()))
} }
runner.server.recvChannel() <- &genericMessage{ runner.server.recvChannel() <- &genericMessage{
Type: typeHeartbeat, Type: typeHeartbeat,

View File

@@ -53,6 +53,24 @@ func (w *WorkerNode) setState(state int32) {
atomic.StoreInt32(&w.State, state) 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) { func (w *WorkerNode) updateHeartbeat(heartbeat int32) {
atomic.StoreInt32(&w.Heartbeat, heartbeat) atomic.StoreInt32(&w.Heartbeat, heartbeat)
} }
@@ -130,8 +148,8 @@ func (w *WorkerNode) getMemoryUsage() float64 {
} }
func (w *WorkerNode) setStream(stream chan *messager.StreamResponse) { func (w *WorkerNode) setStream(stream chan *messager.StreamResponse) {
w.mutex.RLock() w.mutex.Lock()
defer w.mutex.RUnlock() defer w.mutex.Unlock()
w.stream = stream w.stream = stream
} }
@@ -302,26 +320,19 @@ func (s *grpcServer) Register(ctx context.Context, req *messager.RegisterRequest
wn := newWorkerNode(req.NodeID, clientIp, req.Os, req.Arch) wn := newWorkerNode(req.NodeID, clientIp, req.Os, req.Arch)
s.clients.Store(req.NodeID, wn) s.clients.Store(req.NodeID, wn)
log.Warn().Str("worker id", req.NodeID).Msg("worker joined") log.Warn().Str("worker id", req.NodeID).Msg("worker joined")
return &messager.RegisterResponse{Code: "0", Message: "register successfully"}, nil return &messager.RegisterResponse{Code: "0", Message: "register successful"}, nil
} }
func (s *grpcServer) SignOut(_ context.Context, req *messager.SignOutRequest) (*messager.SignOutResponse, error) { func (s *grpcServer) SignOut(_ context.Context, req *messager.SignOutRequest) (*messager.SignOutResponse, error) {
// delete worker information // delete worker information
s.clients.Delete(req.NodeID) s.clients.Delete(req.NodeID)
log.Warn().Str("worker id", req.NodeID).Msg("worker quited") log.Warn().Str("worker id", req.NodeID).Msg("worker quited")
return &messager.SignOutResponse{Code: "0", Message: "sign out successfully"}, nil return &messager.SignOutResponse{Code: "0", Message: "sign out successful"}, nil
} }
func (s *grpcServer) valid(token string) (isValid bool) { func (s *grpcServer) validClientToken(token string) bool {
s.clients.Range(func(key, value interface{}) bool { _, ok := s.clients.Load(token)
if workerInfo, ok := value.(*WorkerNode); ok { return ok
if workerInfo.ID == token {
isValid = true
}
}
return true
})
return
} }
func (s *grpcServer) BidirectionalStreamingMessage(srv messager.Message_BidirectionalStreamingMessageServer) error { func (s *grpcServer) BidirectionalStreamingMessage(srv messager.Message_BidirectionalStreamingMessageServer) error {
@@ -332,7 +343,7 @@ func (s *grpcServer) BidirectionalStreamingMessage(srv messager.Message_Bidirect
return status.Error(codes.Unauthenticated, "missing token header") return status.Error(codes.Unauthenticated, "missing token header")
} }
ok = s.valid(token) ok = s.validClientToken(token)
if !ok { if !ok {
return status.Error(codes.Unauthenticated, "invalid token") return status.Error(codes.Unauthenticated, "invalid token")
} }
@@ -404,7 +415,7 @@ func (s *grpcServer) sendMsg(srv messager.Message_BidirectionalStreamingMessageS
func (s *grpcServer) sendBroadcasts(msg *genericMessage) { func (s *grpcServer) sendBroadcasts(msg *genericMessage) {
s.clients.Range(func(key, value interface{}) bool { s.clients.Range(func(key, value interface{}) bool {
if workerInfo, ok := value.(*WorkerNode); ok { if workerInfo, ok := value.(*WorkerNode); ok {
if workerInfo.getState() == StateQuitting || workerInfo.getState() == StateMissing { if !workerInfo.isAvailable() {
return true return true
} }
workerInfo.getStream() <- &messager.StreamResponse{ workerInfo.getStream() <- &messager.StreamResponse{
@@ -517,10 +528,10 @@ func (s *grpcServer) getClients() *sync.Map {
return s.clients return s.clients
} }
func (s *grpcServer) getClientsLength() (l int) { func (s *grpcServer) getAvailableClientsLength() (l int) {
s.clients.Range(func(key, value interface{}) bool { s.clients.Range(func(key, value interface{}) bool {
if workerInfo, ok := value.(*WorkerNode); ok { if workerInfo, ok := value.(*WorkerNode); ok {
if workerInfo.getState() != StateQuitting && workerInfo.getState() != StateMissing { if workerInfo.isAvailable() {
l++ l++
} }
} }
@@ -528,3 +539,39 @@ func (s *grpcServer) getClientsLength() (l int) {
}) })
return 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
}

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"github.com/httprunner/funplugin" "github.com/httprunner/funplugin"
"github.com/httprunner/funplugin/fungo" "github.com/httprunner/funplugin/fungo"
@@ -24,7 +25,7 @@ const (
const projectInfoFile = "proj.json" // used for ensuring root project const projectInfoFile = "proj.json" // used for ensuring root project
var pluginMap = map[string]funplugin.IPlugin{} // used for reusing plugin instance var pluginMap = sync.Map{} // used for reusing plugin instance
func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err error) { func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err error) {
// plugin file not found // plugin file not found
@@ -37,8 +38,8 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
} }
// reuse plugin instance if it already initialized // reuse plugin instance if it already initialized
if p, ok := pluginMap[pluginPath]; ok { if p, ok := pluginMap.Load(pluginPath); ok {
return p, nil return p.(funplugin.IPlugin), nil
} }
pluginOptions := []funplugin.Option{funplugin.WithLogOn(logOn)} pluginOptions := []funplugin.Option{funplugin.WithLogOn(logOn)}
@@ -74,7 +75,7 @@ func initPlugin(path, venv string, logOn bool) (plugin funplugin.IPlugin, err er
} }
// add plugin instance to plugin map // add plugin instance to plugin map
pluginMap[pluginPath] = plugin pluginMap.Store(pluginPath, plugin)
// report event for initializing plugin // report event for initializing plugin
event := sdk.EventTracking{ event := sdk.EventTracking{

View File

@@ -17,6 +17,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/net/http2" "golang.org/x/net/http2"
"github.com/httprunner/funplugin"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/sdk" "github.com/httprunner/httprunner/v4/hrp/internal/sdk"
) )
@@ -188,11 +189,12 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
// quit all plugins // quit all plugins
defer func() { defer func() {
if len(pluginMap) > 0 { pluginMap.Range(func(key, value interface{}) bool {
for _, plugin := range pluginMap { if plugin, ok := value.(funplugin.IPlugin); ok {
plugin.Quit() plugin.Quit()
} }
} return true
})
}() }()
var runErr error var runErr error
@@ -285,15 +287,17 @@ func (r *HRPRunner) newCaseRunner(testcase *TestCase) (*testCaseRunner, error) {
// load plugin info to testcase config // load plugin info to testcase config
if plugin != nil { if plugin != nil {
pluginPath, _ := locatePlugin(testcase.Config.Path) pluginPath, _ := locatePlugin(testcase.Config.Path)
pluginContent, err := builtin.ReadFile(pluginPath) if runner.parsedConfig.PluginSetting == nil {
if err != nil { pluginContent, err := builtin.ReadFile(pluginPath)
return nil, err if err != nil {
} return nil, err
tp := strings.Split(plugin.Path(), ".") }
runner.parsedConfig.PluginSetting = &PluginConfig{ tp := strings.Split(plugin.Path(), ".")
Path: pluginPath, runner.parsedConfig.PluginSetting = &PluginConfig{
Content: pluginContent, Path: pluginPath,
Type: tp[len(tp)-1], Content: pluginContent,
Type: tp[len(tp)-1],
}
} }
} }

View File

@@ -24,7 +24,7 @@ func removeHashicorpGoPlugin() {
log.Info().Msg("[teardown] remove hashicorp go plugin") log.Info().Msg("[teardown] remove hashicorp go plugin")
os.Remove(tmpl("debugtalk.bin")) os.Remove(tmpl("debugtalk.bin"))
pluginPath, _ := filepath.Abs(tmpl("debugtalk.bin")) pluginPath, _ := filepath.Abs(tmpl("debugtalk.bin"))
delete(pluginMap, pluginPath) pluginMap.Delete(pluginPath)
} }
func buildHashicorpPyPlugin() { func buildHashicorpPyPlugin() {

View File

@@ -191,7 +191,7 @@ func (api *apiHandler) Start(w http.ResponseWriter, r *http.Request) {
return return
} }
req := StartRequestBody{ req := StartRequestBody{
Profile: *api.boomer.GetProfile(), Profile: *boomer.NewProfile(),
} }
err = mapstructure.Decode(data, &req) err = mapstructure.Decode(data, &req)
if err != nil { if err != nil {

View File

@@ -387,13 +387,16 @@ func runStepRequest(r *SessionRunner, step *TStep) (stepResult *StepResult, err
if err != nil { if err != nil {
return stepResult, errors.Wrap(err, "do request failed") return stepResult, errors.Wrap(err, "do request failed")
} }
defer resp.Body.Close() if resp != nil {
defer resp.Body.Close()
}
// decode response body in br/gzip/deflate formats // decode response body in br/gzip/deflate formats
err = decodeResponseBody(resp) err = decodeResponseBody(resp)
if err != nil { if err != nil {
return stepResult, errors.Wrap(err, "decode response body failed") return stepResult, errors.Wrap(err, "decode response body failed")
} }
defer resp.Body.Close()
// log & print response // log & print response
if r.LogOn() { if r.LogOn() {

View File

@@ -104,10 +104,6 @@ func (tc *TCase) ToTestCase(casePath string) (*TestCase, error) {
return nil, errors.New("invalid testcase format, missing teststeps!") return nil, errors.New("invalid testcase format, missing teststeps!")
} }
err := tc.MakeCompat()
if err != nil {
return nil, err
}
if tc.Config == nil { if tc.Config == nil {
tc.Config = &TConfig{Name: "please input testcase name"} tc.Config = &TConfig{Name: "please input testcase name"}
} }
@@ -121,6 +117,11 @@ func (tc *TCase) toTestCase() (*TestCase, error) {
Config: tc.Config, Config: tc.Config,
} }
err := tc.MakeCompat()
if err != nil {
return nil, err
}
// locate project root dir by plugin path // locate project root dir by plugin path
projectRootDir, err := GetProjectRootDirPath(tc.Config.Path) projectRootDir, err := GetProjectRootDirPath(tc.Config.Path)
if err != nil { if err != nil {