From b80d945d06912288933985182603f113b36d1cdb Mon Sep 17 00:00:00 2001 From: TLovell Date: Sun, 12 Apr 2020 22:20:41 -0600 Subject: [PATCH 01/17] Made it so stop only performs if state isn't already stopping/stopped/init in MasterLocustRunner --- locust/runners.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 7c4fff94db..ab7e5f4477 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -405,10 +405,11 @@ def start(self, locust_count, hatch_rate): self.state = STATE_HATCHING def stop(self): - self.state = STATE_STOPPING - for client in self.clients.all: - self.server.send_to_client(Message("stop", None, client.id)) - self.environment.events.test_stop.fire(environment=self.environment) + if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: + self.state = STATE_STOPPING + for client in self.clients.all: + self.server.send_to_client(Message("stop", None, client.id)) + self.environment.events.test_stop.fire(environment=self.environment) def quit(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: From 6f4473dc8bc6151bf293c6fdae1a17295cf43d3b Mon Sep 17 00:00:00 2001 From: TLovell Date: Mon, 13 Apr 2020 01:57:38 -0600 Subject: [PATCH 02/17] Added second fix in web.py --- locust/web.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/locust/web.py b/locust/web.py index 190675138c..20fed000aa 100644 --- a/locust/web.py +++ b/locust/web.py @@ -25,7 +25,7 @@ from io import StringIO from . import runners -from .runners import MasterLocustRunner +from .runners import MasterLocustRunner, STATE_STOPPING, STATE_STOPPED from .stats import failures_csv, median_from_dict, requests_csv, sort_stats from .util.cache import memoize from .util.rounding import proper_round @@ -125,8 +125,11 @@ def swarm(): @app.route('/stop') @self.auth_required_if_enabled def stop(): - environment.runner.stop() - return jsonify({'success':True, 'message': 'Test stopped'}) + if environment.runner.state in [STATE_STOPPING, STATE_STOPPED]: + return jsonify({'success':True, 'message': f"Test already {environment.runner.state}"}) + else: + environment.runner.stop() + return jsonify({'success':True, 'message': 'Test stopped'}) @app.route("/stats/reset") @self.auth_required_if_enabled From a918d81de479b683e64ddd8117e708ee80e17d56 Mon Sep 17 00:00:00 2001 From: TLovell Date: Mon, 13 Apr 2020 02:07:51 -0600 Subject: [PATCH 03/17] made it so test_stop event is fired when all workers quit --- locust/runners.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index ab7e5f4477..f3678e4761 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -491,6 +491,8 @@ def client_listener(self): if msg.node_id in self.clients: del self.clients[msg.node_id] logger.info("Client %r quit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready))) + if self.worker_count == 0: + self.stop() elif msg.type == "exception": self.log_exception(msg.node_id, msg.data["msg"], msg.data["traceback"]) From 00b4da8f1cf5b6ec437f1bc074e3882f87f1c730 Mon Sep 17 00:00:00 2001 From: TLovell Date: Mon, 13 Apr 2020 17:37:27 -0600 Subject: [PATCH 04/17] Added log message --- locust/runners.py | 1 + 1 file changed, 1 insertion(+) diff --git a/locust/runners.py b/locust/runners.py index f3678e4761..cd465a348f 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -492,6 +492,7 @@ def client_listener(self): del self.clients[msg.node_id] logger.info("Client %r quit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready))) if self.worker_count == 0: + logger.info("Last worker quit. Stopping test.") self.stop() elif msg.type == "exception": self.log_exception(msg.node_id, msg.data["msg"], msg.data["traceback"]) From 358ffa54175ad65ab568db2dc85c02c585c80403 Mon Sep 17 00:00:00 2001 From: TLovell Date: Mon, 13 Apr 2020 18:56:21 -0600 Subject: [PATCH 05/17] Removed the alternative api message --- locust/web.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/locust/web.py b/locust/web.py index 95060273ed..e3305b8675 100644 --- a/locust/web.py +++ b/locust/web.py @@ -115,11 +115,9 @@ def swarm(): @app.route('/stop') @self.auth_required_if_enabled def stop(): - if environment.runner.state in [STATE_STOPPING, STATE_STOPPED]: - return jsonify({'success':True, 'message': f"Test already {environment.runner.state}"}) - else: + if environment.runner.state not in [STATE_STOPPING, STATE_STOPPED]: environment.runner.stop() - return jsonify({'success':True, 'message': 'Test stopped'}) + return jsonify({'success':True, 'message': 'Test stopped'}) @app.route("/stats/reset") @self.auth_required_if_enabled From 26c0b013819cc89f9426a3654f4bd0faac12abb0 Mon Sep 17 00:00:00 2001 From: TLovell Date: Mon, 13 Apr 2020 23:29:25 -0600 Subject: [PATCH 06/17] Moved STATE_STOPPED check to the heartbeat method, as well as checking for quitting/missing workers --- locust/runners.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index cd465a348f..7fc479b5ce 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -334,6 +334,10 @@ def hatching(self): @property def running(self): return self.get_by_state(STATE_RUNNING) + + @property + def missing(self): + return self.get_by_state(STATE_MISSING) self.clients = WorkerNodesDict() self.server = rpc.Server(master_bind_host, master_bind_port) @@ -427,6 +431,7 @@ def heartbeat_worker(self): if self.connection_broken: self.reset_connection() continue + for client in self.clients.all: if client.heartbeat < 0 and client.state != STATE_MISSING: logger.info('Worker %s failed to send heartbeat, setting state to missing.' % str(client.id)) @@ -435,6 +440,13 @@ def heartbeat_worker(self): else: client.heartbeat -= 1 + if self.worker_count - len(self.clients.missing) <= 0: + logger.info("No workers remaining, stopping test.") + self.stop() + + if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)): + self.state = STATE_STOPPED + def reset_connection(self): logger.info("Reset connection to slave") try: @@ -491,14 +503,9 @@ def client_listener(self): if msg.node_id in self.clients: del self.clients[msg.node_id] logger.info("Client %r quit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready))) - if self.worker_count == 0: - logger.info("Last worker quit. Stopping test.") - self.stop() elif msg.type == "exception": self.log_exception(msg.node_id, msg.data["msg"], msg.data["traceback"]) - if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)): - self.state = STATE_STOPPED @property def worker_count(self): From 9a414ba9c836fe3e11854737a1357cde1c9fdbb8 Mon Sep 17 00:00:00 2001 From: TLovell Date: Mon, 13 Apr 2020 23:46:49 -0600 Subject: [PATCH 07/17] Added info arg to stop() to log messages on stop only when it actually goes through --- locust/runners.py | 8 +++++--- locust/web.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 7fc479b5ce..3c8e19c59f 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -408,8 +408,11 @@ def start(self, locust_count, hatch_rate): self.state = STATE_HATCHING - def stop(self): + def stop(self, info=None): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: + if info is not None: + logger.info(info) + self.state = STATE_STOPPING for client in self.clients.all: self.server.send_to_client(Message("stop", None, client.id)) @@ -441,8 +444,7 @@ def heartbeat_worker(self): client.heartbeat -= 1 if self.worker_count - len(self.clients.missing) <= 0: - logger.info("No workers remaining, stopping test.") - self.stop() + self.stop(info='No workers remaining, stopping test.') if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)): self.state = STATE_STOPPED diff --git a/locust/web.py b/locust/web.py index e3305b8675..6763fc4057 100644 --- a/locust/web.py +++ b/locust/web.py @@ -116,7 +116,7 @@ def swarm(): @self.auth_required_if_enabled def stop(): if environment.runner.state not in [STATE_STOPPING, STATE_STOPPED]: - environment.runner.stop() + environment.runner.stop(info='Recieved stop request, stopping test.') return jsonify({'success':True, 'message': 'Test stopped'}) @app.route("/stats/reset") From 72a7b876bd0d03c4bd50bb629af09ea071a51325 Mon Sep 17 00:00:00 2001 From: TLovell Date: Tue, 14 Apr 2020 01:02:07 -0600 Subject: [PATCH 08/17] Added heartbeat mocking for broken test case --- locust/test/test_runners.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 0baeb50b4f..86dc73a6fc 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1,5 +1,7 @@ import unittest +from time import time + import gevent from gevent import sleep from gevent.queue import Queue @@ -12,7 +14,7 @@ from locust.exception import LocustError, RPCError, StopLocust from locust.rpc import Message from locust.runners import LocustRunner, LocalLocustRunner, MasterLocustRunner, WorkerNode, \ - WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING + WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING, HEARTBEAT_INTERVAL from locust.stats import RequestStats from locust.test.testcases import LocustTestCase from locust.wait_time import between, constant @@ -386,7 +388,19 @@ def tearDown(self): def get_runner(self): return MasterLocustRunner(self.environment, [], master_bind_host="*", master_bind_port=5557) - + + def mocked_heartbeat(self, server, client_names): + for client_name in client_names: + server.mocked_send(Message('heartbeat', {'state': STATE_RUNNING, 'current_cpu_usage': 50}, client_name)) + + def sleep_with_mocked_heartbeat(self, seconds, server, client_names): + start = time() + while time() - start < seconds: + self.mocked_heartbeat(server, client_names) + sleep(min(HEARTBEAT_INTERVAL, time() - start)) + self.mocked_heartbeat(server, client_names) + + def test_worker_connect(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() @@ -731,14 +745,17 @@ def test_spawn_fewer_locusts_than_workers(self): def test_spawn_locusts_in_stepload_mode(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() + client_names = [] for i in range(5): - server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) + client_name = f"fake_client{i}" + client_names.append(client_name) + server.mocked_send(Message("client_ready", None, client_name)) # 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 2s master.start_stepload(10, 2, 5, 2) # make sure the first step run is started - sleep(0.5) + self.sleep_with_mocked_heartbeat(0.5, server, client_names) self.assertEqual(5, len(server.outbox)) num_clients = 0 @@ -749,7 +766,8 @@ def test_spawn_locusts_in_stepload_mode(self): 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(2) + self.sleep_with_mocked_heartbeat(2, server, client_names) + num_clients = 0 idx = end_of_last_step while idx < len(server.outbox): From 6b2c81ab257068b39cca3d7d1c20c846e59adfa7 Mon Sep 17 00:00:00 2001 From: TLovell Date: Tue, 14 Apr 2020 01:29:59 -0600 Subject: [PATCH 09/17] Alternative mock heatbeat solution, less things to pass around --- locust/test/test_runners.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 86dc73a6fc..65163f3305 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -389,17 +389,12 @@ def tearDown(self): def get_runner(self): return MasterLocustRunner(self.environment, [], master_bind_host="*", master_bind_port=5557) - def mocked_heartbeat(self, server, client_names): - for client_name in client_names: - server.mocked_send(Message('heartbeat', {'state': STATE_RUNNING, 'current_cpu_usage': 50}, client_name)) - - def sleep_with_mocked_heartbeat(self, seconds, server, client_names): + def sleep_with_given_heartbeat(self, seconds, heartbeat): start = time() while time() - start < seconds: - self.mocked_heartbeat(server, client_names) - sleep(min(HEARTBEAT_INTERVAL, time() - start)) - self.mocked_heartbeat(server, client_names) - + heartbeat() + sleep(min(HEARTBEAT_INTERVAL, seconds - time() + start)) + def test_worker_connect(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: @@ -745,17 +740,18 @@ def test_spawn_fewer_locusts_than_workers(self): def test_spawn_locusts_in_stepload_mode(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() - client_names = [] for i in range(5): - client_name = f"fake_client{i}" - client_names.append(client_name) - server.mocked_send(Message("client_ready", None, client_name)) + server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) + + def heartbeat(): + for i in range(5): + server.mocked_send(Message("heartbeat", {'state': STATE_RUNNING, 'current_cpu_usage': 50}, "fake_client%i" % i)) # 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 2s master.start_stepload(10, 2, 5, 2) # make sure the first step run is started - self.sleep_with_mocked_heartbeat(0.5, server, client_names) + self.sleep_with_given_heartbeat(0.5, heartbeat) self.assertEqual(5, len(server.outbox)) num_clients = 0 @@ -766,7 +762,7 @@ def test_spawn_locusts_in_stepload_mode(self): 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 - self.sleep_with_mocked_heartbeat(2, server, client_names) + self.sleep_with_given_heartbeat(2, heartbeat) num_clients = 0 idx = end_of_last_step From 1021d8e3d69d178e6bd6a28732f4ac218839f410 Mon Sep 17 00:00:00 2001 From: TLovell Date: Tue, 14 Apr 2020 23:33:19 -0600 Subject: [PATCH 10/17] Made it so UI updates to stopped form when workers quit --- locust/static/locust.js | 14 ++++++++++---- locust/web.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/locust/static/locust.js b/locust/static/locust.js index cde628705b..bc9f0f0347 100644 --- a/locust/static/locust.js +++ b/locust/static/locust.js @@ -4,14 +4,18 @@ $(window).ready(function() { } }); -$("#box_stop a.stop-button").click(function(event) { - event.preventDefault(); - $.get($(this).attr("href")); +function appearStopped() { $("body").attr("class", "stopped"); $(".box_stop").hide(); $("a.new_test").show(); $("a.edit_test").hide(); $(".user_count").hide(); +} + +$("#box_stop a.stop-button").click(function(event) { + event.preventDefault(); + $.get($(this).attr("href")); + appearStopped() }); $("#box_stop a.reset-button").click(function(event) { @@ -173,6 +177,8 @@ function updateStats() { rpsChart.addValue([total.current_rps, total.current_fail_per_sec]); responseTimeChart.addValue([report.current_response_time_percentile_50, report.current_response_time_percentile_95]); usersChart.addValue([report.user_count]); + } else { + appearStopped() } setTimeout(updateStats, 2000); @@ -187,4 +193,4 @@ function updateExceptions() { setTimeout(updateExceptions, 5000); }); } -updateExceptions(); \ No newline at end of file +updateExceptions(); diff --git a/locust/web.py b/locust/web.py index 6763fc4057..e3305b8675 100644 --- a/locust/web.py +++ b/locust/web.py @@ -116,7 +116,7 @@ def swarm(): @self.auth_required_if_enabled def stop(): if environment.runner.state not in [STATE_STOPPING, STATE_STOPPED]: - environment.runner.stop(info='Recieved stop request, stopping test.') + environment.runner.stop() return jsonify({'success':True, 'message': 'Test stopped'}) @app.route("/stats/reset") From 12f32e7285863f7374f48f1d080798a693a5a658 Mon Sep 17 00:00:00 2001 From: TLovell Date: Wed, 15 Apr 2020 01:21:25 -0600 Subject: [PATCH 11/17] Added .stopping wherever .stopped is in css --- locust/static/style.css | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/locust/static/style.css b/locust/static/style.css index 487eef3b05..7edf64521e 100644 --- a/locust/static/style.css +++ b/locust/static/style.css @@ -139,8 +139,8 @@ a:hover { } .hatching .boxes .box_running {display: block;} .running .boxes .box_running {display: block;} -.stopped .boxes .box_running {display: block;} -.stopped .boxes .box_stop {display: none;} +.stopped .boxes .box_running, .stopping .boxes .box_running {display: block;} +.stopped .boxes .box_stop, .stopping .boxes .box_stop {display: none;} .container { max-width: 1800px; @@ -206,7 +206,7 @@ a:hover { } -.stopped .start { +.stopped .start, .stopping .start { display: none; border-radius: 5px; -moz-border-radius: 5px; @@ -216,7 +216,7 @@ a:hover { box-shadow: 0 0 60px rgba(0,0,0,0.3); } -.stopped .edit {display: none;} +.stopped .edit, .stopping .edit {display: none;} .running .edit, .hatching .edit { display: none; border-radius: 5px; @@ -232,25 +232,25 @@ a:hover { .ready .start {display: block;} .running .status, .hatching .status {display: block;} -.stopped .status {display: block;} +.stopped .status, .stopping .status {display: block;} .ready .status {display: none;} -.stopped .boxes .edit_test, .ready .boxes .edit_test {display: none;} -.stopped .boxes .user_count, .ready .boxes .user_count {display: none;} +.stopped .boxes .edit_test, .stopping .boxes .edit_test, .ready .boxes .edit_test {display: none;} +.stopped .boxes .user_count, .stopping .boxes .user_count, .ready .boxes .user_count {display: none;} .running a.new_test, .ready a.new_test, .hatching a.new_test {display: none;} .running a.new_test {display: none;} -.stopped a.new_test {display: block;} +.stopped a.new_test, .stopping a.new_test {display: block;} .start a.close_link, .edit a.close_link{ position: absolute; right: 10px; top: 10px; } -.stopped .start a.close_link {display: inline;} +.stopped .start a.close_link, .stopping .start a.close_link {display: inline;} .running .start a.close_link, .ready .start a.close_link, .hatching .start a.close_link {display: none;} -.stopped .edit a.close_link, .ready .edit a.close_link {display: none;} +.stopped .edit a.close_link, .stopping .edit a.close_link, .ready .edit a.close_link {display: none;} .running .edit a.close_link, .hatching .edit a.close_link {display: inline;} .stats_label { @@ -461,7 +461,7 @@ ul.tabs li a.current:after { } .running .hostname, .hatching .hostname {display: block;} -.stopped .hostname {display: block;} +.stopped .hostname, .stopping .hostname {display: block;} .ready .hostname {display: none;} .footer { From 4b575b90fb76490b1415e05a75439ea730e42cd8 Mon Sep 17 00:00:00 2001 From: TLovell Date: Wed, 15 Apr 2020 01:25:30 -0600 Subject: [PATCH 12/17] Undid some no-longer-necessary changes --- locust/static/locust.js | 1 + locust/web.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locust/static/locust.js b/locust/static/locust.js index bc9f0f0347..ff3858ac94 100644 --- a/locust/static/locust.js +++ b/locust/static/locust.js @@ -194,3 +194,4 @@ function updateExceptions() { }); } updateExceptions(); + diff --git a/locust/web.py b/locust/web.py index e3305b8675..d455fcaa0d 100644 --- a/locust/web.py +++ b/locust/web.py @@ -15,7 +15,7 @@ from locust import __version__ as version from .exception import AuthCredentialsError -from .runners import MasterLocustRunner, STATE_STOPPED, STATE_STOPPING +from .runners import MasterLocustRunner from .stats import failures_csv, requests_csv, sort_stats from .util.cache import memoize from .util.rounding import proper_round @@ -115,8 +115,7 @@ def swarm(): @app.route('/stop') @self.auth_required_if_enabled def stop(): - if environment.runner.state not in [STATE_STOPPING, STATE_STOPPED]: - environment.runner.stop() + environment.runner.stop() return jsonify({'success':True, 'message': 'Test stopped'}) @app.route("/stats/reset") From 6b511f41c71ee0ff774be43890ccdca0054ff1a4 Mon Sep 17 00:00:00 2001 From: TLovell Date: Wed, 15 Apr 2020 01:27:14 -0600 Subject: [PATCH 13/17] Removed newline in locust.js --- locust/static/locust.js | 1 - 1 file changed, 1 deletion(-) diff --git a/locust/static/locust.js b/locust/static/locust.js index ff3858ac94..bc9f0f0347 100644 --- a/locust/static/locust.js +++ b/locust/static/locust.js @@ -194,4 +194,3 @@ function updateExceptions() { }); } updateExceptions(); - From 82e8b66e94c51dbb70ee486b3c54f787eb09f5d2 Mon Sep 17 00:00:00 2001 From: TLovell Date: Wed, 15 Apr 2020 09:19:04 -0600 Subject: [PATCH 14/17] Added check_stopped function and made new stops specific to reason. Removed info argument from stop --- locust/runners.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 390b2d513a..42f3a476c9 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -414,11 +414,8 @@ def start(self, locust_count, hatch_rate): self.state = STATE_HATCHING - def stop(self, info=None): + def stop(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: - if info is not None: - logger.info(info) - self.state = STATE_STOPPING for client in self.clients.all: self.server.send_to_client(Message("stop", None, client.id)) @@ -433,6 +430,11 @@ def quit(self): self.server.send_to_client(Message("quit", None, client.id)) gevent.sleep(0.5) # wait for final stats report from all workers self.greenlet.kill(block=True) + + def check_stopped(self): + if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)): + self.state = STATE_STOPPED + def heartbeat_worker(self): while True: @@ -446,14 +448,13 @@ def heartbeat_worker(self): logger.info('Worker %s failed to send heartbeat, setting state to missing.' % str(client.id)) client.state = STATE_MISSING client.user_count = 0 + if self.worker_count - len(self.clients.missing) <= 0: + logger.info("The last worker went missing, stopping test.") + self.stop() else: client.heartbeat -= 1 - if self.worker_count - len(self.clients.missing) <= 0: - self.stop(info='No workers remaining, stopping test.') - - if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)): - self.state = STATE_STOPPED + self.check_stopped() def reset_connection(self): logger.info("Reset connection to slave") @@ -511,9 +512,14 @@ def client_listener(self): if msg.node_id in self.clients: del self.clients[msg.node_id] logger.info("Client %r quit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready))) + if self.worker_count - len(self.clients.missing) <= 0: + logger.info("The last worker quit, stopping test.") + self.stop() elif msg.type == "exception": self.log_exception(msg.node_id, msg.data["msg"], msg.data["traceback"]) + self.check_stopped() + @property def worker_count(self): From 7b40b1849d31f0541942f563e743dcecbbe14096 Mon Sep 17 00:00:00 2001 From: TLovell Date: Thu, 16 Apr 2020 00:54:35 -0600 Subject: [PATCH 15/17] Moved stop check to inside if, moved jquery stop set to inside button click, reverted changes to now-working tests --- locust/runners.py | 3 +-- locust/static/locust.js | 4 ++-- locust/test/test_runners.py | 22 ++++------------------ 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 42f3a476c9..f7460ea3d1 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -451,11 +451,10 @@ def heartbeat_worker(self): if self.worker_count - len(self.clients.missing) <= 0: logger.info("The last worker went missing, stopping test.") self.stop() + self.check_stopped() else: client.heartbeat -= 1 - self.check_stopped() - def reset_connection(self): logger.info("Reset connection to slave") try: diff --git a/locust/static/locust.js b/locust/static/locust.js index bc9f0f0347..8aecc0eaf2 100644 --- a/locust/static/locust.js +++ b/locust/static/locust.js @@ -5,7 +5,6 @@ $(window).ready(function() { }); function appearStopped() { - $("body").attr("class", "stopped"); $(".box_stop").hide(); $("a.new_test").show(); $("a.edit_test").hide(); @@ -15,6 +14,7 @@ function appearStopped() { $("#box_stop a.stop-button").click(function(event) { event.preventDefault(); $.get($(this).attr("href")); + $("body").attr("class", "stopped"); appearStopped() }); @@ -178,7 +178,7 @@ function updateStats() { responseTimeChart.addValue([report.current_response_time_percentile_50, report.current_response_time_percentile_95]); usersChart.addValue([report.user_count]); } else { - appearStopped() + appearStopped(); } setTimeout(updateStats, 2000); diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 65163f3305..0baeb50b4f 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1,7 +1,5 @@ import unittest -from time import time - import gevent from gevent import sleep from gevent.queue import Queue @@ -14,7 +12,7 @@ from locust.exception import LocustError, RPCError, StopLocust from locust.rpc import Message from locust.runners import LocustRunner, LocalLocustRunner, MasterLocustRunner, WorkerNode, \ - WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING, HEARTBEAT_INTERVAL + WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING from locust.stats import RequestStats from locust.test.testcases import LocustTestCase from locust.wait_time import between, constant @@ -388,14 +386,7 @@ def tearDown(self): def get_runner(self): return MasterLocustRunner(self.environment, [], master_bind_host="*", master_bind_port=5557) - - def sleep_with_given_heartbeat(self, seconds, heartbeat): - start = time() - while time() - start < seconds: - heartbeat() - sleep(min(HEARTBEAT_INTERVAL, seconds - time() + start)) - - + def test_worker_connect(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() @@ -743,15 +734,11 @@ def test_spawn_locusts_in_stepload_mode(self): for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) - def heartbeat(): - for i in range(5): - server.mocked_send(Message("heartbeat", {'state': STATE_RUNNING, 'current_cpu_usage': 50}, "fake_client%i" % i)) - # 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 2s master.start_stepload(10, 2, 5, 2) # make sure the first step run is started - self.sleep_with_given_heartbeat(0.5, heartbeat) + sleep(0.5) self.assertEqual(5, len(server.outbox)) num_clients = 0 @@ -762,8 +749,7 @@ def heartbeat(): 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 - self.sleep_with_given_heartbeat(2, heartbeat) - + sleep(2) num_clients = 0 idx = end_of_last_step while idx < len(server.outbox): From fc2fbd40bde12ac32967777fc031686345f0b28d Mon Sep 17 00:00:00 2001 From: TLovell Date: Thu, 16 Apr 2020 02:18:49 -0600 Subject: [PATCH 16/17] Added tests --- locust/test/test_runners.py | 43 ++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 0baeb50b4f..9f5397a481 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -12,7 +12,7 @@ from locust.exception import LocustError, RPCError, StopLocust from locust.rpc import Message from locust.runners import LocustRunner, LocalLocustRunner, MasterLocustRunner, WorkerNode, \ - WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING + WorkerLocustRunner, STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_MISSING, STATE_STOPPED from locust.stats import RequestStats from locust.test.testcases import LocustTestCase from locust.wait_time import between, constant @@ -452,6 +452,47 @@ def test_master_marks_downed_workers_as_missing(self): # print(master.clients['fake_client'].__dict__) assert master.clients['fake_client'].state == STATE_MISSING + def test_last_worker_quitting_stops_test(self): + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: + master = self.get_runner() + server.mocked_send(Message("client_ready", None, "fake_client1")) + server.mocked_send(Message("client_ready", None, "fake_client2")) + + master.start(1, 2) + server.mocked_send(Message("hatching", None, "fake_client1")) + server.mocked_send(Message("hatching", None, "fake_client2")) + + server.mocked_send(Message("quit", None, "fake_client1")) + sleep(1) + self.assertEqual(1, len(master.clients.all)) + self.assertNotEqual(STATE_STOPPED, master.state, "Not all workers quit but test stopped anyway.") + + server.mocked_send(Message("quit", None, "fake_client2")) + sleep(1) + self.assertEqual(0, len(master.clients.all)) + self.assertEqual(STATE_STOPPED, master.state, "All workers quit but test didn't stop.") + + def test_last_worker_missing_stops_test(self): + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: + master = self.get_runner() + server.mocked_send(Message("client_ready", None, "fake_client1")) + server.mocked_send(Message("client_ready", None, "fake_client2")) + + master.start(1, 2) + server.mocked_send(Message("hatching", None, "fake_client1")) + server.mocked_send(Message("hatching", None, "fake_client2")) + + sleep(3) + server.mocked_send(Message("heartbeat", {'state': STATE_RUNNING, 'current_cpu_usage': 50}, "fake_client1")) + + sleep(3) + self.assertEqual(1, len(master.clients.missing)) + self.assertNotEqual(STATE_STOPPED, master.state, "Not all workers went missing but test stopped anyway.") + + sleep(3) + self.assertEqual(2, len(master.clients.missing)) + self.assertEqual(STATE_STOPPED, master.state, "All workers went missing but test didn't stop.") + def test_master_total_stats(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() From 59f117f071fb12282febc45b984c28f182291bd0 Mon Sep 17 00:00:00 2001 From: TLovell Date: Thu, 16 Apr 2020 08:44:29 -0600 Subject: [PATCH 17/17] made new tests faster --- locust/test/test_runners.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 18f6b50324..070fc1c8da 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -463,15 +463,16 @@ def test_last_worker_quitting_stops_test(self): server.mocked_send(Message("hatching", None, "fake_client2")) server.mocked_send(Message("quit", None, "fake_client1")) - sleep(1) + sleep(0) self.assertEqual(1, len(master.clients.all)) self.assertNotEqual(STATE_STOPPED, master.state, "Not all workers quit but test stopped anyway.") server.mocked_send(Message("quit", None, "fake_client2")) - sleep(1) + sleep(0) self.assertEqual(0, len(master.clients.all)) self.assertEqual(STATE_STOPPED, master.state, "All workers quit but test didn't stop.") + @mock.patch("locust.runners.HEARTBEAT_INTERVAL", new=0.1) def test_last_worker_missing_stops_test(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() @@ -482,14 +483,14 @@ def test_last_worker_missing_stops_test(self): server.mocked_send(Message("hatching", None, "fake_client1")) server.mocked_send(Message("hatching", None, "fake_client2")) - sleep(3) + sleep(0.3) server.mocked_send(Message("heartbeat", {'state': STATE_RUNNING, 'current_cpu_usage': 50}, "fake_client1")) - sleep(3) + sleep(0.3) self.assertEqual(1, len(master.clients.missing)) self.assertNotEqual(STATE_STOPPED, master.state, "Not all workers went missing but test stopped anyway.") - sleep(3) + sleep(0.3) self.assertEqual(2, len(master.clients.missing)) self.assertEqual(STATE_STOPPED, master.state, "All workers went missing but test didn't stop.")