From 1c5039697a38e6001b3c9141f9d87d46d6593b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E8=81=AA?= Date: Mon, 11 Jul 2022 11:56:36 +0800 Subject: [PATCH] change: update docs --- docs/CHANGELOG.md | 38 +++++++++-- docs/cmd/hrp_boom.md | 3 +- examples/demo-empty-project/proj.json | 4 +- examples/demo-with-go-plugin/proj.json | 2 +- .../demo-with-py-plugin/.debugtalk_gen.py | 2 +- examples/demo-with-py-plugin/proj.json | 2 +- examples/demo-without-plugin/proj.json | 4 +- hrp/boomer.go | 5 +- hrp/cmd/boom.go | 9 +-- hrp/internal/boomer/boomer.go | 3 + hrp/internal/boomer/client_grpc.go | 2 +- hrp/internal/boomer/runner.go | 68 ++++++++----------- hrp/internal/boomer/runner_test.go | 4 +- hrp/server.go | 13 ++-- 14 files changed, 93 insertions(+), 66 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index bf0ea847..61780740 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,17 +1,25 @@ # Release History +## v4.2.0 (2022-07-22) + +**go version** + +- feat: support multi-machine collaborative distributed load testing + ## v4.1.7 (2022-07-18) **go version** -- fix: using `@FILEPATH` to indicate the path of the file - feat: support indicating type and filename when uploading file - feat: support to infer MIME type of the file automatically - feat: support omitting websocket url if not necessary - feat: support multiple websocket connections each session -- fix: optimize websocket step initialization - feat: support convert curl command(s) to testcase(s) - feat: support run curl as subcommand of run/boom/convert +- fix: optimize websocket step initialization +- fix: using `@FILEPATH` to indicate the path of the file +- fix: reuse plugin instance if it already initialized +- fix: deep copy api step to avoid data racing ## v4.1.6 (2022-07-04) @@ -276,7 +284,8 @@ - feat: implement `transaction` mechanism for load test - feat: continue running next step when failure occurs with `--continue-on-failure` flag, default to failfast - feat: report GA events with version -- feat: run load test with the given limit and burst as rate limiter, use `--spawn-count`, `--spawn-rate` and `--request-increase-rate` flag +- feat: run load test with the given limit and burst as rate limiter, use `--spawn-count`, `--spawn-rate` + and `--request-increase-rate` flag - feat: report runner state to prometheus - refactor: fork [boomer] as submodule initially and made a lot of changes - change: update API models @@ -330,7 +339,8 @@ ## 3.1.8 (2022-03-22) -- feat: add `--profile` flag for har2case to support overwrite headers/cookies with specified yaml/json configuration file +- feat: add `--profile` flag for har2case to support overwrite headers/cookies with specified yaml/json configuration + file - feat: support variable and function in response extract expression - fix: keep negative index in jmespath unchanged when converting pytest files, e.g. body.users[-1] - fix: variable should not start with digit @@ -462,9 +472,9 @@ **Changed** - change: override variables - (1) testcase: session variables > step variables > config variables - (2) testsuite: testcase variables > config variables - (3) testsuite testcase variables > testcase config variables + (1) testcase: session variables > step variables > config variables + (2) testsuite: testcase variables > config variables + (3) testsuite testcase variables > testcase config variables **Fixed** @@ -648,17 +658,31 @@ reference: [v2-changelog] [hrp]: https://github.com/httprunner/hrp + [hashicorp/go-plugin]: https://github.com/hashicorp/go-plugin + [go plugin]: https://pkg.go.dev/plugin + [docs repo]: https://github.com/httprunner/httprunner.github.io + [zerolog]: https://github.com/rs/zerolog + [jmespath]: https://jmespath.org/ + [mkdocs]: https://www.mkdocs.org/ + [github-actions]: https://github.com/httprunner/hrp/actions + [boomer]: github.com/myzhan/boomer + [sentry sdk]: https://github.com/getsentry/sentry-go + [pushgateway]: https://github.com/prometheus/pushgateway + [locust]: https://locust.io/ + [black]: https://github.com/psf/black + [loguru]: https://github.com/Delgan/loguru + [v2-changelog]: https://github.com/httprunner/httprunner/blob/v2/docs/CHANGELOG.md diff --git a/docs/cmd/hrp_boom.md b/docs/cmd/hrp_boom.md index a51770b5..f61722a1 100644 --- a/docs/cmd/hrp_boom.md +++ b/docs/cmd/hrp_boom.md @@ -21,7 +21,7 @@ hrp boom [flags] ### Options ``` - --autostart Starts the test immediately (without disabling the web UI). Use --spawn-count and --spawn-rate to control user count and run time + --autostart Starts the test immediately (without disabling the web UI). Use --spawn-count and --spawn-rate to control user count and increase rate --cpu-profile string Enable CPU profiling. --cpu-profile-duration duration CPU profile duration. (default 30s) --disable-compression Disable compression @@ -36,6 +36,7 @@ hrp boom [flags] --master-bind-host string Interfaces (hostname, ip) that hrp master should bind to. Only used when running with --master. Defaults to * (all available interfaces). (default "127.0.0.1") --master-bind-port int Port that hrp master should bind to. Only used when running with --master. Defaults to 5557. (default 5557) --master-host string Host or IP address of hrp master for distributed load testing. (default "127.0.0.1") + --master-http-address string Interfaces (ip:port) that hrp master should control by user. Only used when running with --master. Defaults to *:9771. (default ":9771") --master-port int The port to connect to that is used by the hrp master for distributed load testing. (default 5557) --max-rps int Max RPS that boomer can generate, disabled by default. --mem-profile string Enable memory profiling. diff --git a/examples/demo-empty-project/proj.json b/examples/demo-empty-project/proj.json index fe59965d..b2b376f6 100644 --- a/examples/demo-empty-project/proj.json +++ b/examples/demo-empty-project/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-empty-project", - "create_time": "2022-07-04T14:54:33.795693+08:00", - "hrp_version": "v4.1.5" + "create_time": "2022-07-11T11:45:29.942532+08:00", + "hrp_version": "v4.1.6" } diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json index 13b1eab0..1899c546 100644 --- a/examples/demo-with-go-plugin/proj.json +++ b/examples/demo-with-go-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-go-plugin", - "create_time": "2022-07-06T13:57:04.054424+08:00", + "create_time": "2022-07-11T11:44:36.214909+08:00", "hrp_version": "v4.1.6" } diff --git a/examples/demo-with-py-plugin/.debugtalk_gen.py b/examples/demo-with-py-plugin/.debugtalk_gen.py index 70910180..9588e35e 100644 --- a/examples/demo-with-py-plugin/.debugtalk_gen.py +++ b/examples/demo-with-py-plugin/.debugtalk_gen.py @@ -20,4 +20,4 @@ if __name__ == "__main__": funppy.register("concatenate", concatenate) funppy.register("setup_hook_example", setup_hook_example) funppy.register("teardown_hook_example", teardown_hook_example) - funppy.serve() + funppy.serve() \ No newline at end of file diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json index 73d9a31c..f2336020 100644 --- a/examples/demo-with-py-plugin/proj.json +++ b/examples/demo-with-py-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-py-plugin", - "create_time": "2022-07-06T13:57:04.482633+08:00", + "create_time": "2022-07-11T11:44:37.021634+08:00", "hrp_version": "v4.1.6" } diff --git a/examples/demo-without-plugin/proj.json b/examples/demo-without-plugin/proj.json index 24b61c18..50c06186 100644 --- a/examples/demo-without-plugin/proj.json +++ b/examples/demo-without-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-without-plugin", - "create_time": "2022-07-04T14:54:33.495643+08:00", - "hrp_version": "v4.1.5" + "create_time": "2022-07-11T11:45:29.800018+08:00", + "hrp_version": "v4.1.6" } diff --git a/hrp/boomer.go b/hrp/boomer.go index 460fd2ed..91094341 100644 --- a/hrp/boomer.go +++ b/hrp/boomer.go @@ -208,6 +208,7 @@ func (b *HRPBoomer) runTestCases(testCases []*TCase, profile *boomer.Profile) { testcases = append(testcases, tesecase) } + b.SetProfile(profile) b.InitBoomer() log.Info().Interface("testcases", testcases).Interface("profile", profile).Msg("run tasks successful") @@ -222,7 +223,7 @@ func (b *HRPBoomer) rebalanceBoomer(profile *boomer.Profile) { log.Info().Interface("profile", profile).Msg("rebalance tasks successful") } -func (b *HRPBoomer) PollTasks() { +func (b *HRPBoomer) PollTasks(ctx context.Context) { for { select { case task := <-b.Boomer.GetTasksChan(): @@ -240,6 +241,8 @@ func (b *HRPBoomer) PollTasks() { case <-b.Boomer.GetCloseChan(): return + case <-ctx.Done(): + return } } } diff --git a/hrp/cmd/boom.go b/hrp/cmd/boom.go index 5df6cd26..a13232c3 100644 --- a/hrp/cmd/boom.go +++ b/hrp/cmd/boom.go @@ -71,7 +71,7 @@ var boomCmd = &cobra.Command{ if boomArgs.autoStart { hrpBoomer.InitBoomer() } else { - go hrpBoomer.StartServer() + go hrpBoomer.StartServer(ctx, boomArgs.masterHttpAddress) } go hrpBoomer.PollTestCases(ctx) hrpBoomer.RunMaster() @@ -79,9 +79,8 @@ var boomCmd = &cobra.Command{ if boomArgs.ignoreQuit { hrpBoomer.SetIgnoreQuit() } - go hrpBoomer.PollTasks() + go hrpBoomer.PollTasks(ctx) hrpBoomer.RunWorker() - time.Sleep(3 * time.Second) case "standalone": if venv != "" { hrpBoomer.SetPython3Venv(venv) @@ -102,6 +101,7 @@ type BoomArgs struct { masterPort int masterBindHost string masterBindPort int + masterHttpAddress string autoStart bool expectWorkers int expectWorkersMaxWait int @@ -129,11 +129,12 @@ func init() { boomCmd.Flags().BoolVar(&boomArgs.master, "master", false, "master of distributed testing") boomCmd.Flags().StringVar(&boomArgs.masterBindHost, "master-bind-host", "127.0.0.1", "Interfaces (hostname, ip) that hrp master should bind to. Only used when running with --master. Defaults to * (all available interfaces).") boomCmd.Flags().IntVar(&boomArgs.masterBindPort, "master-bind-port", 5557, "Port that hrp master should bind to. Only used when running with --master. Defaults to 5557.") + boomCmd.Flags().StringVar(&boomArgs.masterHttpAddress, "master-http-address", ":9771", "Interfaces (ip:port) that hrp master should control by user. Only used when running with --master. Defaults to *:9771.") boomCmd.Flags().BoolVar(&boomArgs.worker, "worker", false, "worker of distributed testing") boomCmd.Flags().BoolVar(&boomArgs.ignoreQuit, "ignore-quit", false, "ignores quit from master (only when --worker is used)") boomCmd.Flags().StringVar(&boomArgs.masterHost, "master-host", "127.0.0.1", "Host or IP address of hrp master for distributed load testing.") boomCmd.Flags().IntVar(&boomArgs.masterPort, "master-port", 5557, "The port to connect to that is used by the hrp master for distributed load testing.") - boomCmd.Flags().BoolVar(&boomArgs.autoStart, "autostart", false, "Starts the test immediately (without disabling the web UI). Use --spawn-count and --spawn-rate to control user count and run time") + boomCmd.Flags().BoolVar(&boomArgs.autoStart, "autostart", false, "Starts the test immediately (without disabling the web UI). Use --spawn-count and --spawn-rate to control user count and increase rate") boomCmd.Flags().IntVar(&boomArgs.expectWorkers, "expect-workers", 1, "How many workers master should expect to connect before starting the test (only when --autostart is used)") boomCmd.Flags().IntVar(&boomArgs.expectWorkersMaxWait, "expect-workers-max-wait", 0, "How many workers master should expect to connect before starting the test (only when --autostart is used") } diff --git a/hrp/internal/boomer/boomer.go b/hrp/internal/boomer/boomer.go index 5b6a7f2a..2dc40056 100644 --- a/hrp/internal/boomer/boomer.go +++ b/hrp/internal/boomer/boomer.go @@ -461,6 +461,9 @@ func (b *Boomer) Start(Args *Profile) error { if b.masterRunner.isStarted() { return errors.New("already started") } + if b.masterRunner.getState() == StateStopping { + return errors.New("Please wait for all workers to finish") + } b.SetSpawnCount(Args.SpawnCount) b.SetSpawnRate(Args.SpawnRate) b.SetProfile(Args) diff --git a/hrp/internal/boomer/client_grpc.go b/hrp/internal/boomer/client_grpc.go index 434b88e9..f96f789e 100644 --- a/hrp/internal/boomer/client_grpc.go +++ b/hrp/internal/boomer/client_grpc.go @@ -216,7 +216,7 @@ func (c *grpcClient) newBiStreamClient() (err error) { return err } c.config.setBiStreamClient(biStream) - println("successful to establish bidirectional stream with master, press Ctrl+c to quit.\n") + println("successful to establish bidirectional stream with master, press Ctrl+c to quit.") return nil } diff --git a/hrp/internal/boomer/runner.go b/hrp/internal/boomer/runner.go index 100a7bca..869244dd 100644 --- a/hrp/internal/boomer/runner.go +++ b/hrp/internal/boomer/runner.go @@ -10,12 +10,10 @@ import ( "sync/atomic" "time" + "github.com/go-errors/errors" "github.com/httprunner/httprunner/v4/hrp/internal/boomer/grpc/messager" "github.com/httprunner/httprunner/v4/hrp/internal/builtin" "github.com/jinzhu/copier" - - "github.com/go-errors/errors" - "github.com/olekukonko/tablewriter" "github.com/rs/zerolog/log" ) @@ -200,15 +198,15 @@ type runner struct { controller *Controller loop *Loop // specify loop count for testcase, count = loopCount * spawnCount - // rebalance spawn + // dynamically balance boomer running parameters rebalance chan bool - // all running workers(goroutines) will select on this channel. - // close this channel will stop all running workers. + // stop signals the run goroutine should shutdown. stopChan chan bool - + // all running workers(goroutines) will select on this channel. + // stopping is closed by run goroutine on shutdown. stoppingChan chan bool - + // done is closed when all goroutines from start() complete. doneChan chan bool reportChan chan bool @@ -216,10 +214,10 @@ type runner struct { // close this channel will stop all goroutines used in runner. closeChan chan bool - // wgMu blocks concurrent waitgroup mutation while server stopping + // wgMu blocks concurrent waitgroup mutation while boomer stopping wgMu sync.RWMutex - // wg is used to wait for the goroutines that depends on the server state - // to exit when stopping the server. + // wg is used to wait for all running workers(goroutines) that depends on the boomer state + // to exit when stopping the boomer. wg sync.WaitGroup outputs []Output @@ -544,15 +542,20 @@ func (r *runner) statsStart() { func (r *runner) stop() { // stop previous goroutines without blocking // those goroutines will exit when r.safeRun returns - r.Stop() + r.gracefulStop() if r.rateLimitEnabled { r.rateLimiter.Stop() } r.updateState(StateStopped) } -// HardStop stops the server without coordination with other members in the cluster. -func (r *runner) hardStop() { +// gracefulStop stops the boomer gracefully, and shuts down the running goroutine. +// gracefulStop should be called after a start(), otherwise it will block forever. +// When stopping leader, Stop transfers its leadership to one of its peers +// before stopping the boomer. +// gracefulStop terminates the boomer and performs any necessary finalization. +// Do and Process cannot be called after Stop has been invoked. +func (r *runner) gracefulStop() { select { case r.stopChan <- true: case <-r.doneChan: @@ -561,30 +564,16 @@ func (r *runner) hardStop() { <-r.doneChan } -// Stop stops the server gracefully, and shuts down the running goroutine. -// Stop should be called after a Start(s), otherwise it will block forever. -// When stopping leader, Stop transfers its leadership to one of its peers -// before stopping the server. -// Stop terminates the Server and performs any necessary finalization. -// Do and Process cannot be called after Stop has been invoked. -func (r *runner) Stop() { - r.hardStop() -} +// StopNotify returns a channel that receives a bool type value +// when the runner is stopped. +func (r *runner) StopNotify() <-chan bool { return r.doneChan } -// StopNotify returns a channel that receives a empty struct -// when the server is stopped. -func (r *runner) StopNotify() <-chan bool { return r.stopChan } - -// DoneNotify returns a channel that receives a empty struct -// when the server is stopped. -func (r *runner) DoneNotify() <-chan bool { return r.doneChan } - -// StoppingNotify returns a channel that receives a empty struct -// when the server is being stopped. +// StoppingNotify returns a channel that receives a bool type value +// when the runner is being stopped. func (r *runner) StoppingNotify() <-chan bool { return r.stoppingChan } -// RebalanceNotify returns a channel that receives a empty struct -// when the server is being stopped. +// RebalanceNotify returns a channel that receives a bool type value +// when the runner is being rebalance. func (r *runner) RebalanceNotify() <-chan bool { return r.rebalance } func (r *runner) getState() int32 { @@ -758,6 +747,7 @@ func (r *workerRunner) onMessage(msg *genericMessage) { r.onSpawnMessage(msg) case "quit": if r.ignoreQuit { + log.Warn().Msg("master already quit, waiting to reconnect master.") break } r.close() @@ -777,6 +767,7 @@ func (r *workerRunner) onMessage(msg *genericMessage) { case "quit": r.stop() if r.ignoreQuit { + log.Warn().Msg("master already quit, waiting to reconnect master.") break } r.close() @@ -788,6 +779,7 @@ func (r *workerRunner) onMessage(msg *genericMessage) { r.onSpawnMessage(msg) case "quit": if r.ignoreQuit { + log.Warn().Msg("master already quit, waiting to reconnect master.") break } r.close() @@ -815,13 +807,13 @@ func (r *workerRunner) startListener() { // run worker service func (r *workerRunner) run() { - println("\n========================= HttpRunner Worker for Distributed Load Testing ========================= ") + println("==================== HttpRunner Worker for Distributed Load Testing ==================== ") r.updateState(StateInit) r.client = newClient(r.masterHost, r.masterPort, r.nodeID) println(fmt.Sprintf("ready to connect master to %s:%d", r.masterHost, r.masterPort)) err := r.client.start() if err != nil { - log.Error().Err(err).Msg(fmt.Sprintf("failed to connect to master(%s:%d) with error %v\n", r.masterHost, r.masterPort)) + log.Error().Err(err).Msg(fmt.Sprintf("failed to connect to master(%s:%d)", r.masterHost, r.masterPort)) } if err = r.client.register(r.client.config.ctx); err != nil { @@ -904,7 +896,7 @@ func (r *workerRunner) start() { r.rateLimiter.Start() } - r.once.Do(r.outputOnStart) + r.outputOnStart() go r.spawnWorkers(r.getSpawnCount(), r.getSpawnRate(), r.stoppingChan, r.spawnComplete) diff --git a/hrp/internal/boomer/runner_test.go b/hrp/internal/boomer/runner_test.go index 8c139d58..2a250800 100644 --- a/hrp/internal/boomer/runner_test.go +++ b/hrp/internal/boomer/runner_test.go @@ -126,13 +126,13 @@ func TestStopNotify(t *testing.T) { close(r.doneChan) }() - notifier := r.DoneNotify() + notifier := r.StopNotify() select { case <-notifier: t.Fatalf("received unexpected stop notification") default: } - r.Stop() + r.gracefulStop() select { case <-notifier: default: diff --git a/hrp/server.go b/hrp/server.go index 1f811bd7..a42d9a33 100644 --- a/hrp/server.go +++ b/hrp/server.go @@ -163,7 +163,7 @@ func (api *apiHandler) Index(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not Found", http.StatusNotFound) return } - w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' camo.githubusercontent.com") + w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' www.httprunner.com") fmt.Fprintf(w, "Welcome to httprunner page!") } @@ -315,23 +315,26 @@ func (api *apiHandler) Handler() http.Handler { func (apiHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} -func (b *HRPBoomer) StartServer() { +func (b *HRPBoomer) StartServer(ctx context.Context, addr string) { h := b.NewAPIHandler() mux := h.Handler() server := &http.Server{ - Addr: ":9771", + Addr: addr, Handler: mux, } go func() { - <-b.GetCloseChan() + select { + case <-ctx.Done(): + case <-b.GetCloseChan(): + } if err := server.Shutdown(context.Background()); err != nil { log.Fatal("shutdown server:", err) } }() - log.Println("Starting HTTP server...") + log.Println(fmt.Sprintf("starting HTTP server (%v), please use the API to control master", server.Addr)) err := server.ListenAndServe() if err != nil { if err == http.ErrServerClosed {