From 75cd3c7a599e0f1aecfea018bb64a64b6cca9947 Mon Sep 17 00:00:00 2001 From: delulu Date: Fri, 19 Apr 2019 15:19:22 +0800 Subject: [PATCH 01/10] support step load mode in locust agent --- locust/main.py | 90 ++++++++++++++++++++++++++++++++--------------- locust/runners.py | 33 +++++++++++++++-- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/locust/main.py b/locust/main.py index ee9dac037b..02f8c9ca7c 100644 --- a/locust/main.py +++ b/locust/main.py @@ -165,7 +165,7 @@ def parse_options(args=None, default_config_files=['~/.locust.conf','locust.conf '-t', '--run-time', help="Stop after the specified amount of time, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with --no-web" ) - + # skip logging setup parser.add_argument( '--skip-log-setup', @@ -175,6 +175,27 @@ def parse_options(args=None, default_config_files=['~/.locust.conf','locust.conf help="Disable Locust's logging setup. Instead, the configuration is provided by the Locust test or Python defaults." ) + # Enable Step Load mode + parser.add_argument( + '--step-load', + action='store_true', + help="Enable Step Load mode to monitor how performance metrics varies when user load increases. Requires --step-clients and --step-time to be specified." + ) + + # Number of clients to incease by Step + parser.add_argument( + '--step-clients', + type='int', + default=1, + help="Client count to increase by step in Step Load mode. Only used together with --step-load" + ) + + # Time limit of each step + parser.add_argument( + '--step-time', + help="Step duration in Step Load mode, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used together with --step-load" + ) + # log level parser.add_argument( '--loglevel', '-L', @@ -450,6 +471,9 @@ def main(): if not options.no_web: logger.error("The --run-time argument can only be used together with --no-web") sys.exit(1) + if options.slave: + logger.error("--run-time should be specified on the master node, and not on slave nodes") + sys.exit(1) try: options.run_time = parse_timespan(options.run_time) except ValueError: @@ -462,42 +486,50 @@ def timelimit_stop(): runners.locust_runner.quit() gevent.spawn_later(options.run_time, timelimit_stop) - if not options.no_web and not options.slave: - # spawn web greenlet - logger.info("Starting web monitor at http://%s:%s" % (options.web_host or "*", options.port)) - main_greenlet = gevent.spawn(web.start, locust_classes, options) + if options.step_time: + if not options.step_load: + logger.error("The --step-time argument can only be used together with --step-load") + sys.exit(1) + if options.slave: + logger.error("--step-time should be specified on the master node, and not on slave nodes") + sys.exit(1) + try: + options.step_time = parse_timespan(options.step_time) + except ValueError: + logger.error("Valid --step-time formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.") + sys.exit(1) - if not options.master and not options.slave: - runners.locust_runner = LocalLocustRunner(locust_classes, options) - # spawn client spawning/hatching greenlet - if options.no_web: - runners.locust_runner.start_hatching(wait=True) - main_greenlet = runners.locust_runner.greenlet - if options.run_time: - spawn_run_time_limit_greenlet() - elif options.master: + if options.master: runners.locust_runner = MasterLocustRunner(locust_classes, options) - if options.no_web: - while len(runners.locust_runner.clients.ready) int(self.total_clients): + logger.info('Step Load is finished.') + break + self.start_hatching(current_num_clients, self.hatch_rate) + logger.info('Step loading: start hatch job of %d locust.' % (current_num_clients)) + gevent.sleep(self.step_duration) + def client_listener(self): while True: client_id, msg = self.server.recv_from_client() From ee655e30724a4f9538f004c2769fe1b3c94e8f73 Mon Sep 17 00:00:00 2001 From: delulu Date: Thu, 14 Nov 2019 15:31:39 +0800 Subject: [PATCH 02/10] support step load mode in locust ui --- locust/templates/index.html | 16 ++++++++++++++-- locust/web.py | 14 +++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/locust/templates/index.html b/locust/templates/index.html index a394221890..f84aa3149e 100644 --- a/locust/templates/index.html +++ b/locust/templates/index.html @@ -30,7 +30,7 @@ {% if is_distributed %}
SLAVES
-
0
+
{{slave_count}}
{% endif %}
@@ -58,7 +58,7 @@

Start new Locust swarm

- +

@@ -69,6 +69,12 @@

Start new Locust swarm

{% endif %}
+ {% if is_step_load %} + +
+ +
+ {% endif %}
@@ -86,6 +92,12 @@

Change the locust count



+ {% if is_step_load %} + +
+ +
+ {% endif %}
diff --git a/locust/web.py b/locust/web.py index b90c8b6365..f3af3425c1 100644 --- a/locust/web.py +++ b/locust/web.py @@ -27,6 +27,7 @@ from .stats import failures_csv, median_from_dict, requests_csv, sort_stats, stats_history_csv from .util.cache import memoize from .util.rounding import proper_round +from .util.time import parse_timespan logger = logging.getLogger(__name__) @@ -60,6 +61,8 @@ def index(): else: host = None + is_step_load = runners.locust_runner.step_load + return render_template("index.html", state=runners.locust_runner.state, is_distributed=is_distributed, @@ -67,16 +70,25 @@ def index(): version=version, host=host, override_host_warning=override_host_warning, + slave_count=slave_count, + is_step_load=is_step_load ) @app.route('/swarm', methods=["POST"]) def swarm(): assert request.method == "POST" - + is_step_load = runners.locust_runner.step_load locust_count = int(request.form["locust_count"]) hatch_rate = float(request.form["hatch_rate"]) if (request.form.get("host")): runners.locust_runner.host = str(request.form["host"]) + + if is_step_load: + step_locust_count = int(request.form["step_locust_count"]) + step_duration = parse_timespan(str(request.form["step_duration"])) + runners.locust_runner.start_stepload(locust_count, hatch_rate, step_locust_count, step_duration) + return jsonify({'success': True, 'message': 'Swarming started in Step Load Mode', 'host': runners.locust_runner.host}) + runners.locust_runner.start_hatching(locust_count, hatch_rate) return jsonify({'success': True, 'message': 'Swarming started', 'host': runners.locust_runner.host}) From fce097d9b93ae41b53c4f577af10d797ba04845e Mon Sep 17 00:00:00 2001 From: delulu Date: Fri, 19 Apr 2019 16:59:45 +0800 Subject: [PATCH 03/10] add test case for step load --- locust/test/test_runners.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 3939be55f8..97df684cb4 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -59,6 +59,7 @@ def __init__(self): self.heartbeat_liveness = 3 self.heartbeat_interval = 0.01 self.stop_timeout = None + self.step_load = True def reset_stats(self): pass @@ -413,6 +414,25 @@ class MyTestLocust(Locust): self.assertEqual(2, num_clients, "Total number of locusts that would have been spawned is not 2") + def test_spawn_locusts_in_stepload_mode(self): + class MyTestLocust(Locust): + pass + + with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server: + master = MasterLocustRunner(MyTestLocust, self.options) + for i in range(5): + server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) + + master.start_stepload(2, 1, 1, 3) + sleep(1) + self.assertEqual(5, len(server.outbox)) + + num_clients = 0 + for _, msg in server.outbox: + num_clients += Message.unserialize(msg).data["num_clients"] + + self.assertEqual(1, num_clients, "Total number of locusts that would have been spawned is not 1") + def test_exception_in_task(self): class HeyAnException(Exception): pass From ad06017e0a70f493757ea65226efce666dce76e5 Mon Sep 17 00:00:00 2001 From: delulu Date: Mon, 4 Nov 2019 16:54:10 +0800 Subject: [PATCH 04/10] update test case for step load check --- locust/runners.py | 3 +-- locust/test/test_runners.py | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 2f81d13530..7c293c57d0 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -356,8 +356,7 @@ def start_stepload(self, locust_count, hatch_rate, step_locust_count, step_durat if self.stepload_greenlet: logger.info("There is an ongoing swarming in Step Load mode, will stop it now.") self.greenlet.killone(self.stepload_greenlet) - - logger.info("Start a new swarming in Step Load mode: %d locusts, %d delta locusts in step, %ds step duration " % (locust_count, step_locust_count, step_duration)) + logger.info("Start a new swarming in Step Load mode: total locust count of %d, hatch rate of %d, step locust count of %d, step duration of %d " % (locust_count, hatch_rate, step_locust_count, step_duration)) self.state = STATE_INIT self.stepload_greenlet = self.greenlet.spawn(self.stepload_worker) self.stepload_greenlet.link_exception(callback=self.noop) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 97df684cb4..3d37f0016f 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -57,7 +57,7 @@ def __init__(self): self.master_bind_host = '*' self.master_bind_port = 5557 self.heartbeat_liveness = 3 - self.heartbeat_interval = 0.01 + self.heartbeat_interval = 3 self.stop_timeout = None self.step_load = True @@ -422,16 +422,30 @@ class MyTestLocust(Locust): master = MasterLocustRunner(MyTestLocust, self.options) for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) - - master.start_stepload(2, 1, 1, 3) + + # start a new swarming in Step Load mode: total locust count of 10, hatch rate of 2, step locust count of 5, step duration of 5s + master.start_stepload(10, 2, 5, 5) + + # make sure the first step run is started sleep(1) self.assertEqual(5, len(server.outbox)) - + num_clients = 0 + end_of_last_step = len(server.outbox) for _, msg in server.outbox: num_clients += Message.unserialize(msg).data["num_clients"] - self.assertEqual(1, num_clients, "Total number of locusts that would have been spawned is not 1") + self.assertEqual(5, num_clients, "Total number of locusts that would have been spawned for first step is not 5") + + # make sure the first step run is complete + sleep(5) + num_clients = 0 + idx = end_of_last_step + while idx < len(server.outbox): + msg = server.outbox[idx][1] + num_clients += Message.unserialize(msg).data["num_clients"] + idx += 1 + self.assertEqual(10, num_clients, "Total number of locusts that would have been spawned for second step is not 10") def test_exception_in_task(self): class HeyAnException(Exception): From 3401e54194add65e60d6635e0f9e7e600b53db07 Mon Sep 17 00:00:00 2001 From: delulu Date: Mon, 4 Nov 2019 17:55:30 +0800 Subject: [PATCH 05/10] resolve test failure --- locust/main.py | 2 +- locust/test/test_runners.py | 4 ++-- locust/web.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/locust/main.py b/locust/main.py index 02f8c9ca7c..b7bda8852d 100644 --- a/locust/main.py +++ b/locust/main.py @@ -185,7 +185,7 @@ def parse_options(args=None, default_config_files=['~/.locust.conf','locust.conf # Number of clients to incease by Step parser.add_argument( '--step-clients', - type='int', + type=int, default=1, help="Client count to increase by step in Step Load mode. Only used together with --step-load" ) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 3d37f0016f..a37e9b327d 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -57,7 +57,7 @@ def __init__(self): self.master_bind_host = '*' self.master_bind_port = 5557 self.heartbeat_liveness = 3 - self.heartbeat_interval = 3 + self.heartbeat_interval = 1 self.stop_timeout = None self.step_load = True @@ -219,7 +219,7 @@ class MyTestLocust(Locust): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = MasterLocustRunner(MyTestLocust, self.options) server.mocked_send(Message("client_ready", None, "fake_client")) - sleep(0.1) + sleep(6) # print(master.clients['fake_client'].__dict__) assert master.clients['fake_client'].state == STATE_MISSING diff --git a/locust/web.py b/locust/web.py index f3af3425c1..74a2599e11 100644 --- a/locust/web.py +++ b/locust/web.py @@ -27,7 +27,7 @@ from .stats import failures_csv, median_from_dict, requests_csv, sort_stats, stats_history_csv from .util.cache import memoize from .util.rounding import proper_round -from .util.time import parse_timespan +from .util.timespan import parse_timespan logger = logging.getLogger(__name__) From 7eec53d508556063745516226a8a8e16de48af1b Mon Sep 17 00:00:00 2001 From: delulu Date: Mon, 4 Nov 2019 20:11:53 +0800 Subject: [PATCH 06/10] support step load in both standalone run and distributed run --- locust/runners.py | 56 +++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 7c293c57d0..40ddb102fb 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -210,6 +210,30 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): else: self.spawn_locusts(wait=wait) + def start_stepload(self, locust_count, hatch_rate, step_locust_count, step_duration): + self.total_clients = locust_count + self.hatch_rate = hatch_rate + self.step_clients_growth = step_locust_count + self.step_duration = step_duration + if self.stepload_greenlet: + logger.info("There is an ongoing swarming in Step Load mode, will stop it now.") + self.greenlet.killone(self.stepload_greenlet) + logger.info("Start a new swarming in Step Load mode: total locust count of %d, hatch rate of %d, step locust count of %d, step duration of %d " % (locust_count, hatch_rate, step_locust_count, step_duration)) + self.state = STATE_INIT + self.stepload_greenlet = self.greenlet.spawn(self.stepload_worker) + self.stepload_greenlet.link_exception(callback=self.noop) + + def stepload_worker(self): + current_num_clients = 0 + while self.state == STATE_INIT or self.state == STATE_HATCHING or self.state == STATE_RUNNING: + current_num_clients += self.step_clients_growth + if current_num_clients > int(self.total_clients): + logger.info('Step Load is finished.') + break + self.start_hatching(current_num_clients, self.hatch_rate) + logger.info('Step loading: start hatch job of %d locust.' % (current_num_clients)) + gevent.sleep(self.step_duration) + def stop(self): # if we are currently hatching locusts we need to kill the hatching greenlet first if self.hatching_greenlet and not self.hatching_greenlet.ready(): @@ -229,6 +253,10 @@ def log_exception(self, node_id, msg, formatted_tb): row["nodes"].add(node_id) self.exceptions[key] = row + def noop(self, *args, **kwargs): + """ Used to link() greenlets to in order to be compatible with gevent 1.0 """ + pass + class LocalLocustRunner(LocustRunner): def __init__(self, locust_classes, options): super(LocalLocustRunner, self).__init__(locust_classes, options) @@ -252,10 +280,6 @@ def __init__(self, locust_classes, options): self.master_bind_port = options.master_bind_port self.heartbeat_liveness = options.heartbeat_liveness self.heartbeat_interval = options.heartbeat_interval - - def noop(self, *args, **kwargs): - """ Used to link() greenlets to in order to be compatible with gevent 1.0 """ - pass class SlaveNode(object): def __init__(self, id, state=STATE_INIT, heartbeat_liveness=3): @@ -348,19 +372,6 @@ def start_hatching(self, locust_count, hatch_rate): self.state = STATE_HATCHING - def start_stepload(self, locust_count, hatch_rate, step_locust_count, step_duration): - self.total_clients = locust_count - self.hatch_rate = hatch_rate - self.step_clients_growth = step_locust_count - self.step_duration = step_duration - if self.stepload_greenlet: - logger.info("There is an ongoing swarming in Step Load mode, will stop it now.") - self.greenlet.killone(self.stepload_greenlet) - logger.info("Start a new swarming in Step Load mode: total locust count of %d, hatch rate of %d, step locust count of %d, step duration of %d " % (locust_count, hatch_rate, step_locust_count, step_duration)) - self.state = STATE_INIT - self.stepload_greenlet = self.greenlet.spawn(self.stepload_worker) - self.stepload_greenlet.link_exception(callback=self.noop) - def stop(self): self.state = STATE_STOPPING for client in self.clients.all: @@ -384,17 +395,6 @@ def heartbeat_worker(self): else: client.heartbeat -= 1 - def stepload_worker(self): - current_num_clients = 0 - while self.state == STATE_INIT or self.state == STATE_HATCHING or self.state == STATE_RUNNING: - current_num_clients += self.step_clients_growth - if current_num_clients > int(self.total_clients): - logger.info('Step Load is finished.') - break - self.start_hatching(current_num_clients, self.hatch_rate) - logger.info('Step loading: start hatch job of %d locust.' % (current_num_clients)) - gevent.sleep(self.step_duration) - def client_listener(self): while True: client_id, msg = self.server.recv_from_client() From 2e148ba83a345e06723c099b8a8c026fa708904d Mon Sep 17 00:00:00 2001 From: delulu Date: Wed, 6 Nov 2019 18:17:00 +0800 Subject: [PATCH 07/10] make sure locusts are spawned in local run otherwise it will exit unexpectedly --- locust/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/locust/main.py b/locust/main.py index b7bda8852d..bceedb0e07 100644 --- a/locust/main.py +++ b/locust/main.py @@ -522,6 +522,8 @@ def timelimit_stop(): runners.locust_runner.start_stepload(options.num_clients, options.hatch_rate, options.step_clients, options.step_time) elif not options.slave: runners.locust_runner.start_hatching(options.num_clients, options.hatch_rate) + # make locusts are spawned + time.sleep(1) elif not options.slave: # spawn web greenlet logger.info("Starting web monitor at http://%s:%s" % (options.web_host or "*", options.port)) From 05ba11e0f2a1a50d91fc236dbd8da42967b7337b Mon Sep 17 00:00:00 2001 From: delulu Date: Wed, 6 Nov 2019 21:26:23 +0800 Subject: [PATCH 08/10] add invalid condition check in step load run --- locust/runners.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 40ddb102fb..7d8ebdf922 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -211,10 +211,14 @@ def start_hatching(self, locust_count=None, hatch_rate=None, wait=False): self.spawn_locusts(wait=wait) def start_stepload(self, locust_count, hatch_rate, step_locust_count, step_duration): + if locust_count < step_locust_count: + logger.error("Invalid parameters: total locust count of %d is smaller than step locust count of %d" % (locust_count, step_locust_count)) + return self.total_clients = locust_count self.hatch_rate = hatch_rate self.step_clients_growth = step_locust_count self.step_duration = step_duration + if self.stepload_greenlet: logger.info("There is an ongoing swarming in Step Load mode, will stop it now.") self.greenlet.killone(self.stepload_greenlet) From 21ea8d6cba4677dc7c441207e177d2fddff3552e Mon Sep 17 00:00:00 2001 From: delulu Date: Thu, 14 Nov 2019 15:12:06 +0800 Subject: [PATCH 09/10] add swarm test case for step load mode --- locust/test/test_web.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locust/test/test_web.py b/locust/test/test_web.py index 1fcbe23cc6..4beee543ce 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -19,7 +19,7 @@ ALTERNATIVE_HOST = 'http://localhost' SWARM_DATA_WITH_HOST = {'locust_count': 5, 'hatch_rate': 5, 'host': ALTERNATIVE_HOST} SWARM_DATA_WITH_NO_HOST = {'locust_count': 5, 'hatch_rate': 5} - +SWARM_DATA_WITH_STEP_LOAD = {"locust_count":5, "hatch_rate":2, "step_locust_count":2, "step_duration": "2m"} class TestWebUI(LocustTestCase): def setUp(self): @@ -210,3 +210,9 @@ class MyLocust2(Locust): self.assertEqual(200, response.status_code) self.assertNotIn("http://example.com", response.content.decode("utf-8")) self.assertIn("setting this will override the host on all Locust classes", response.content.decode("utf-8")) + + def test_swarm_in_step_load_mode(self): + runners.locust_runner.step_load = True + response = requests.post("http://127.0.0.1:%i/swarm" % self.web_port, SWARM_DATA_WITH_STEP_LOAD) + self.assertEqual(200, response.status_code) + self.assertIn("Step Load Mode", response.text) From 14db39d981ae6bb5ec74ac6334a4d3a698bbb810 Mon Sep 17 00:00:00 2001 From: delulu Date: Fri, 6 Dec 2019 18:10:03 +0800 Subject: [PATCH 10/10] fix test failure --- locust/test/test_runners.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index a37e9b327d..8da464c394 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -418,7 +418,7 @@ def test_spawn_locusts_in_stepload_mode(self): class MyTestLocust(Locust): pass - with mock.patch("locust.rpc.rpc.Server", mocked_rpc_server()) as server: + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = MasterLocustRunner(MyTestLocust, self.options) for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) @@ -433,7 +433,7 @@ class MyTestLocust(Locust): num_clients = 0 end_of_last_step = len(server.outbox) for _, msg in server.outbox: - num_clients += Message.unserialize(msg).data["num_clients"] + num_clients += msg.data["num_clients"] self.assertEqual(5, num_clients, "Total number of locusts that would have been spawned for first step is not 5") @@ -443,7 +443,7 @@ class MyTestLocust(Locust): idx = end_of_last_step while idx < len(server.outbox): msg = server.outbox[idx][1] - num_clients += Message.unserialize(msg).data["num_clients"] + num_clients += msg.data["num_clients"] idx += 1 self.assertEqual(10, num_clients, "Total number of locusts that would have been spawned for second step is not 10")