From f4f87118e652871887aa9ca5a3d1d0f7b1ed8a11 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 8 Nov 2020 11:19:35 -0500 Subject: [PATCH 001/139] Use random.choices in locust.runners.Runner.weight_users instead of custom logic --- locust/runners.py | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 699e0103c6..e139ef2df5 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -137,34 +137,11 @@ def weight_users(self, amount): Distributes the amount of users for each WebLocust-class according to it's weight returns a list "bucket" with the weighted users """ - bucket = [] - weight_sum = sum([user.weight for user in self.user_classes]) - residuals = {} for user in self.user_classes: if self.environment.host is not None: user.host = self.environment.host - - # create users depending on weight - percent = user.weight / float(weight_sum) - num_users = int(round(amount * percent)) - bucket.extend([user for x in range(num_users)]) - # used to keep track of the amount of rounding was done if we need - # to add/remove some instances from bucket - residuals[user] = amount * percent - round(amount * percent) - if len(bucket) < amount: - # We got too few User classes in the bucket, so we need to create a few extra users, - # and we do this by iterating over each of the User classes - starting with the one - # where the residual from the rounding was the largest - and creating one of each until - # we get the correct amount - for user in [l for l, r in sorted(residuals.items(), key=lambda x: x[1], reverse=True)][ - : amount - len(bucket) - ]: - bucket.append(user) - elif len(bucket) > amount: - # We've got too many users due to rounding errors so we need to remove some - for user in [l for l, r in sorted(residuals.items(), key=lambda x: x[1])][: len(bucket) - amount]: - bucket.remove(user) - + weights = [user.weight for user in self.user_classes] + bucket = random.choices(self.user_classes, weights=weights, k=amount) return bucket def spawn_users(self, spawn_count, spawn_rate, wait=False): @@ -194,7 +171,7 @@ def spawn(): self.environment.events.spawning_complete.fire(user_count=len(self.user_greenlets)) return - user_class = bucket.pop(random.randint(0, len(bucket) - 1)) + user_class = bucket.pop() occurrence_count[user_class.__name__] += 1 new_user = user_class(self.environment) new_user.start(self.user_greenlets) @@ -240,7 +217,7 @@ def stop_users(self, user_count, stop_rate=None): stop_group = Group() while True: - user_to_stop: User = to_stop.pop(random.randint(0, len(to_stop) - 1)) + user_to_stop: User = to_stop.pop() logger.debug("Stopping %s" % user_to_stop._greenlet.name) if user_to_stop._greenlet is greenlet.getcurrent(): # User called runner.quit(), so dont block waiting for killing to finish" From 5ec80900a82766a88d5fcbb3dac256be0ae750ef Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 13 Nov 2020 11:15:24 -0500 Subject: [PATCH 002/139] wip: users distribution & dispatch algorithm --- locust/dispatch.py | 226 +++++++ locust/distribution.py | 147 +++++ locust/test/test_dispatch.py | 1002 ++++++++++++++++++++++++++++++ locust/test/test_distribution.py | 271 ++++++++ 4 files changed, 1646 insertions(+) create mode 100644 locust/dispatch.py create mode 100644 locust/distribution.py create mode 100644 locust/test/test_dispatch.py create mode 100644 locust/test/test_distribution.py diff --git a/locust/dispatch.py b/locust/dispatch.py new file mode 100644 index 0000000000..80dbe8ed52 --- /dev/null +++ b/locust/dispatch.py @@ -0,0 +1,226 @@ +import itertools +import math +import time +from copy import deepcopy +from typing import ( + Dict, + Generator, + List, +) + +import gevent + +from locust.runners import WorkerNode + + +def dispatch_users( + worker_nodes: List[WorkerNode], + user_class_occurrences: Dict[str, int], + spawn_rate: float, +) -> Generator[Dict[str, Dict[str, int]], None, None]: + initial_dispatched_users = { + worker_node.id: { + user_class: worker_node.user_class_occurrences.get(user_class, 0) + for user_class in user_class_occurrences.keys() + } + for worker_node in worker_nodes + } + + balanced_users = balance_users_among_workers( + worker_nodes, + user_class_occurrences, + ) + + effective_balanced_users = { + worker_node.id: { + user_class: max( + 0, + balanced_users[worker_node.id][user_class] - initial_dispatched_users[worker_node.id][user_class], + ) + for user_class in user_class_occurrences.keys() + } + for worker_node in worker_nodes + } + + wait_between_dispatch = math.ceil(1 / spawn_rate) + + number_of_users_each_dispatch = math.ceil(spawn_rate) + + dispatched_users = deepcopy(initial_dispatched_users) + + less_users_than_desired = all( + sum(x[user_class] for x in dispatched_users.values()) + < sum(x[user_class] for x in effective_balanced_users.values()) + for user_class in user_class_occurrences.keys() + ) + + if less_users_than_desired: + while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: + found = False + number_of_users_in_current_dispatch = 0 + for user_class in user_class_occurrences.keys(): + if all(x[user_class] == 0 for x in effective_balanced_users.values()): + continue + for worker_node_id in itertools.cycle(effective_balanced_users.keys()): + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 + if number_of_users_in_current_dispatch == number_of_users_each_dispatch: + found = True + break + if all(x[user_class] == 0 for x in effective_balanced_users.values()): + break + if found: + break + + ts = time.time() + yield { + worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) + for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) + } + delta = time.time() - ts + gevent.sleep(max(0.0, wait_between_dispatch - delta)) + + else: + while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: + found = False + if all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): + break + number_of_users_in_current_dispatch = 0 + for user_class in user_class_occurrences.keys(): + if all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, user_class): + continue + if all(x[user_class] == 0 for x in effective_balanced_users.values()): + continue + for worker_node_id in itertools.cycle(effective_balanced_users.keys()): + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 + if number_of_users_in_current_dispatch == number_of_users_each_dispatch: + found = True + break + if all(x[user_class] == 0 for x in effective_balanced_users.values()): + break + if found: + break + + if not found: + # We have no more users to dispatch and + # number_of_users_in_current_dispatch < number_of_users_each_dispatch, + # thus we need to break out of the while loop + break + + if all( + sum(x[user_class] for x in dispatched_users.values()) + >= sum(x[user_class] for x in balanced_users.values()) + for user_class in user_class_occurrences.keys() + ): + # TODO: Explain + break + + ts = time.time() + yield { + worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) + for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) + } + delta = time.time() - ts + gevent.sleep(max(0.0, wait_between_dispatch - delta)) + + # If we are here, it means we have an excess of users for one or more user classes. + # Hence, we need to dispatch a last set of users that will bring the desired users + # distribution to the desired one. + # TODO: Explain why we don't stop the users at "spawn_rate" + # and why we stop the excess users once at the end. + yield balanced_users + + +# TODO: test +def all_users_have_been_dispatched( + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + user_class_occurrences: Dict[str, int], +) -> bool: + return all( + sum(x[user_class] for x in dispatched_users.values()) + > sum(x[user_class] for x in effective_balanced_users.values()) + for user_class in user_class_occurrences.keys() + ) + + +# TODO: test +def all_users_of_current_class_have_been_dispatched( + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + user_class: str, +) -> bool: + return ( + sum(x[user_class] for x in dispatched_users.values()) + > sum(x[user_class] for x in effective_balanced_users.values()) + ) + + +# TODO: test +def add_dispatched_users( + dispatched_users1: Dict[str, Dict[str, int]], + dispatched_users2: Dict[str, Dict[str, int]], +) -> Dict[str, Dict[str, int]]: + worker_node_ids = sorted( + set(dispatched_users1.keys()).union( + dispatched_users2.keys() + ) + ) + user_classes = sorted( + set(y for x in dispatched_users1.values() for y in x.keys()).union( + y for x in dispatched_users2.values() for y in x.keys() + ) + ) + return { + worker_node_id: { + user_class: ( + dispatched_users1.get(worker_node_id, {}).get(user_class, 0) + + dispatched_users2.get(worker_node_id, {}).get(user_class, 0) + ) + for user_class in user_classes + } + for worker_node_id in worker_node_ids + } + + +def current_dispatch_ready( + balanced_users: Dict[str, Dict[str, int]], + dispatched_users: Dict[str, Dict[str, int]], + number_of_users_each_dispatch: int, +) -> bool: + if sum(sum(x.values()) for x in dispatched_users.values()) == number_of_users_each_dispatch: + return True + if sum(sum(x.values()) for x in balanced_users.values()) == 0: + return True + return False + + +def balance_users_among_workers( + worker_nodes: List[WorkerNode], + user_class_occurrences: Dict[str, int], +) -> Dict[str, Dict[str, int]]: + balanced_users = { + worker_node.id: { + user_class: 0 for user_class in sorted(user_class_occurrences.keys()) + } for worker_node in worker_nodes + } + + user_class_occurrences = user_class_occurrences.copy() + + for user_class in sorted(user_class_occurrences.keys()): + if sum(user_class_occurrences.values()) == 0: + break + for worker_node in itertools.cycle(worker_nodes): + if user_class_occurrences[user_class] == 0: + break + balanced_users[worker_node.id][user_class] += 1 + user_class_occurrences[user_class] -= 1 + + return balanced_users diff --git a/locust/distribution.py b/locust/distribution.py new file mode 100644 index 0000000000..2a866e02b9 --- /dev/null +++ b/locust/distribution.py @@ -0,0 +1,147 @@ +import math +from operator import attrgetter +from typing import ( + Dict, + List, + Type, +) + +from locust import User + + +def weight_users( + user_classes: List[Type[User]], + number_of_users: int, +) -> Dict[str, int]: + """ + Compute users to spawn + + :param user_classes: the list of user class + :param number_of_users: total number of users + :return: the new set of users to run + """ + assert number_of_users >= 0 + + user_classes = sorted(user_classes, key=lambda u: u.__name__) + + user_class_occurrences = {user_class.__name__: 0 for user_class in user_classes} + + if number_of_users <= len(user_classes): + user_class_occurrences.update({ + user_class.__name__: 1 + for user_class in sorted( + user_classes, + key=lambda user_class: user_class.weight, + reverse=True, + )[:number_of_users] + }) + return user_class_occurrences + + weights = list(map(attrgetter("weight"), user_classes)) + user_class_occurrences = { + user_class.__name__: round(relative_weight * number_of_users) or 1 + for user_class, relative_weight in zip(user_classes, (weight / sum(weights) for weight in weights)) + } + + if sum(user_class_occurrences.values()) == number_of_users: + return user_class_occurrences + + elif sum(user_class_occurrences.values()) > number_of_users: + user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} + _recursive_remove_users( + user_classes, + number_of_users, + user_class_occurrences.copy(), + user_class_occurrences_candidates, + ) + return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] + + elif sum(user_class_occurrences.values()) < number_of_users: + user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} + _recursive_add_users( + user_classes, + number_of_users, + user_class_occurrences.copy(), + user_class_occurrences_candidates, + ) + return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] + + +def _recursive_add_users( + user_classes: List[Type[User]], + number_of_users: int, + user_class_occurrences_candidate: Dict[str, int], + user_class_occurrences_candidates: Dict[float, Dict[str, int]], +): + if sum(user_class_occurrences_candidate.values()) == number_of_users: + distance = distance_from_desired_distribution( + user_classes, + user_class_occurrences_candidate, + ) + if distance not in user_class_occurrences_candidates: + user_class_occurrences_candidates[distance] = user_class_occurrences_candidate + return + elif sum(user_class_occurrences_candidate.values()) > number_of_users: + return + + for user_class in user_classes: + user_class_occurrences_candidate_ = user_class_occurrences_candidate.copy() + user_class_occurrences_candidate_[user_class.__name__] += 1 + _recursive_add_users( + user_classes, + number_of_users, + user_class_occurrences_candidate_, + user_class_occurrences_candidates, + ) + + +def _recursive_remove_users( + user_classes: List[Type[User]], + number_of_users: int, + user_class_occurrences_candidate: Dict[str, int], + user_class_occurrences_candidates: Dict[float, Dict[str, int]], +): + if sum(user_class_occurrences_candidate.values()) == number_of_users: + distance = distance_from_desired_distribution( + user_classes, + user_class_occurrences_candidate, + ) + if distance not in user_class_occurrences_candidates: + user_class_occurrences_candidates[distance] = user_class_occurrences_candidate + return + elif sum(user_class_occurrences_candidate.values()) < number_of_users: + return + + for user_class in sorted(user_classes, key=lambda u: u.__name__, reverse=True): + if user_class_occurrences_candidate[user_class.__name__] == 1: + continue + user_class_occurrences_candidate_ = user_class_occurrences_candidate.copy() + user_class_occurrences_candidate_[user_class.__name__] -= 1 + _recursive_remove_users( + user_classes, + number_of_users, + user_class_occurrences_candidate_, + user_class_occurrences_candidates, + ) + + +def distance_from_desired_distribution( + user_classes: List[Type[User]], + user_class_occurrences: Dict[str, int], +) -> float: + user_class_2_actual_percentage = { + user_class: 100 * occurrences / sum(user_class_occurrences.values()) + for user_class, occurrences in user_class_occurrences.items() + } + + user_class_2_expected_percentage = { + user_class.__name__: 100 * user_class.weight / sum(map(attrgetter("weight"), user_classes)) + for user_class in user_classes + } + + differences = [ + user_class_2_actual_percentage[user_class] - expected_percentage + for user_class, expected_percentage in user_class_2_expected_percentage.items() + ] + + return math.sqrt(math.fsum(map(lambda x: x**2, differences))) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py new file mode 100644 index 0000000000..23dc1e5ad3 --- /dev/null +++ b/locust/test/test_dispatch.py @@ -0,0 +1,1002 @@ +import time +import unittest + +from locust.dispatch import ( + balance_users_among_workers, + dispatch_users, +) +from locust.runners import WorkerNode + + +class TestBalanceUsersAmongWorkers(unittest.TestCase): + maxDiff = None + + def test_balance_users_among_1_worker(self): + worker_node1 = WorkerNode("1") + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + ) + self.assertDictEqual(balanced_users, {"1": {"User1": 3, "User2": 3, "User3": 3}}) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1], + user_class_occurrences={"User1": 5, "User2": 4, "User3": 2}, + ) + self.assertDictEqual(balanced_users, {"1": {"User1": 5, "User2": 4, "User3": 2}}) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1], + user_class_occurrences={"User1": 1, "User2": 1, "User3": 1}, + ) + self.assertDictEqual(balanced_users, {"1": {"User1": 1, "User2": 1, "User3": 1}}) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1], + user_class_occurrences={"User1": 1, "User2": 1, "User3": 0}, + ) + self.assertDictEqual(balanced_users, {"1": {"User1": 1, "User2": 1, "User3": 0}}) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1], + user_class_occurrences={"User1": 0, "User2": 0, "User3": 0}, + ) + self.assertDictEqual(balanced_users, {"1": {"User1": 0, "User2": 0, "User3": 0}}) + + def test_balance_users_among_3_workers(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + ) + expected_balanced_users = { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 5, "User2": 4, "User3": 2}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 2, "User3": 1}, + "2": {"User1": 2, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 1, "User2": 1, "User3": 1}, + ) + expected_balanced_users = { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 1, "User2": 1, "User3": 0}, + ) + expected_balanced_users = { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 0, "User2": 0, "User3": 0}, + ) + expected_balanced_users = { + "1": {"User1": 0, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + '3': {"User1": 0, "User2": 0, "User3": 0}, + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + +class TestDispatchUsersWithWorkersWithoutUsers(unittest.TestCase): + maxDiff = None + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.5, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=1, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=3, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=4, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=9, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + +class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): + maxDiff = None + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.5, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=1, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=3, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=4, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=9, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + +class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): + maxDiff = None + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.5, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(1.99 <= delta <= 2.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=1, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0.99 <= delta <= 1.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=3, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=4, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=9, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + +class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): + maxDiff = None + + def test_dispatch_users_to_3_workers(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} + + for spawn_rate in [0.5, 1, 2, 3, 4, 9]: + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=spawn_rate, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py new file mode 100644 index 0000000000..5a8209f9cf --- /dev/null +++ b/locust/test/test_distribution.py @@ -0,0 +1,271 @@ +import unittest + +from locust import User +from locust.distribution import weight_users + + +class TestDistribution(unittest.TestCase): + maxDiff = None + + def test_distribution_equal_weights_and_fewer_amount_than_user_classes(self): + class User1(User): + weight = 1 + + class User2(User): + weight = 1 + + class User3(User): + weight = 1 + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=0, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 0}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=1, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 0, "User3": 0}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=2, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 0}) + + def test_distribution_equal_weights(self): + class User1(User): + weight = 1 + + class User2(User): + weight = 1 + + class User3(User): + weight = 1 + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=3, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=4, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 1, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=5, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 2, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=6, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 2, "User3": 2}) + + def test_distribution_unequal_and_unique_weights_and_fewer_amount_than_user_classes(self): + class User1(User): + weight = 1 + + class User2(User): + weight = 2 + + class User3(User): + weight = 3 + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=0, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 0}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=1, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=2, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 1, "User3": 1}) + + def test_distribution_unequal_and_unique_weights(self): + class User1(User): + weight = 1 + + class User2(User): + weight = 2 + + class User3(User): + weight = 3 + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=3, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=4, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 2}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=5, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 2}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=6, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 3}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=10, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 3, "User3": 5}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=11, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 4, "User3": 5}) + + def test_distribution_unequal_and_non_unique_weights_and_fewer_amount_than_user_classes(self): + class User1(User): + weight = 1 + + class User2(User): + weight = 2 + + class User3(User): + weight = 2 + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=0, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 0}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=1, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 1, "User3": 0}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=2, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 1, "User3": 1}) + + def test_distribution_unequal_and_non_unique_weights(self): + class User1(User): + weight = 1 + + class User2(User): + weight = 2 + + class User3(User): + weight = 2 + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=3, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=4, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 1}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=5, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 2}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=6, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 3, "User3": 2}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=10, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, 'User2': 4, "User3": 4}) + + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3], + number_of_users=11, + ) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 5, "User3": 4}) + + def test_distribution_large_number_of_users(self): + class User1(User): + weight = 5 + + class User2(User): + weight = 55 + + class User3(User): + weight = 37 + + class User4(User): + weight = 2 + + class User5(User): + weight = 97 + + class User6(User): + weight = 41 + + class User7(User): + weight = 33 + + class User8(User): + weight = 19 + + class User9(User): + weight = 19 + + class User10(User): + weight = 34 + + class User11(User): + weight = 78 + + class User12(User): + weight = 76 + + class User13(User): + weight = 28 + + class User14(User): + weight = 62 + + class User15(User): + weight = 69 + + number_of_users = 1044523783783 + user_class_occurrences = weight_users( + user_classes=[User1, User2, User3, User4, User5, User6, User7, User8, User9, User10, User11, User12, User13, User14, User15], + number_of_users=number_of_users, + ) + self.assertEqual(sum(user_class_occurrences.values()), number_of_users) From 394e02421dc796c452b3e297e06fa7ae3d915e1e Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 13 Nov 2020 11:55:58 -0500 Subject: [PATCH 003/139] Code cleanup --- locust/dispatch.py | 93 +++++++++++++++++++----------------- locust/test/test_dispatch.py | 34 ++++++++++++- 2 files changed, 80 insertions(+), 47 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 80dbe8ed52..5eca3bee11 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -26,11 +26,13 @@ def dispatch_users( for worker_node in worker_nodes } + # This represents the desired users distribution balanced_users = balance_users_among_workers( worker_nodes, user_class_occurrences, ) + # This represents the desired users distribution minus the already running users effective_balanced_users = { worker_node.id: { user_class: max( @@ -44,10 +46,12 @@ def dispatch_users( wait_between_dispatch = math.ceil(1 / spawn_rate) - number_of_users_each_dispatch = math.ceil(spawn_rate) + number_of_users_per_dispatch = math.ceil(spawn_rate) dispatched_users = deepcopy(initial_dispatched_users) + # The amount of users in each user class + # is less than the desired amount less_users_than_desired = all( sum(x[user_class] for x in dispatched_users.values()) < sum(x[user_class] for x in effective_balanced_users.values()) @@ -56,23 +60,18 @@ def dispatch_users( if less_users_than_desired: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - found = False number_of_users_in_current_dispatch = 0 for user_class in user_class_occurrences.keys(): if all(x[user_class] == 0 for x in effective_balanced_users.values()): continue - for worker_node_id in itertools.cycle(effective_balanced_users.keys()): - if effective_balanced_users[worker_node_id][user_class] == 0: - continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 - number_of_users_in_current_dispatch += 1 - if number_of_users_in_current_dispatch == number_of_users_each_dispatch: - found = True - break - if all(x[user_class] == 0 for x in effective_balanced_users.values()): - break - if found: + done, number_of_users_in_current_dispatch = distribute_current_user_class_among_workers( + dispatched_users, + effective_balanced_users, + user_class, + number_of_users_in_current_dispatch, + number_of_users_per_dispatch, + ) + if done: break ts = time.time() @@ -85,32 +84,26 @@ def dispatch_users( else: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - found = False + done = False if all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): break number_of_users_in_current_dispatch = 0 for user_class in user_class_occurrences.keys(): if all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, user_class): continue - if all(x[user_class] == 0 for x in effective_balanced_users.values()): - continue - for worker_node_id in itertools.cycle(effective_balanced_users.keys()): - if effective_balanced_users[worker_node_id][user_class] == 0: - continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 - number_of_users_in_current_dispatch += 1 - if number_of_users_in_current_dispatch == number_of_users_each_dispatch: - found = True - break - if all(x[user_class] == 0 for x in effective_balanced_users.values()): - break - if found: + done, number_of_users_in_current_dispatch = distribute_current_user_class_among_workers( + dispatched_users, + effective_balanced_users, + user_class, + number_of_users_in_current_dispatch, + number_of_users_per_dispatch, + ) + if done: break - if not found: + if not done: # We have no more users to dispatch and - # number_of_users_in_current_dispatch < number_of_users_each_dispatch, + # number_of_users_in_current_dispatch < number_of_users_per_dispatch, # thus we need to break out of the while loop break @@ -138,6 +131,28 @@ def dispatch_users( yield balanced_users +def distribute_current_user_class_among_workers( + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + user_class: str, + number_of_users_in_current_dispatch: int, + number_of_users_per_dispatch: int, +): + done = False + for worker_node_id in itertools.cycle(effective_balanced_users.keys()): + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 + if number_of_users_in_current_dispatch == number_of_users_per_dispatch: + done = True + break + if all(x[user_class] == 0 for x in effective_balanced_users.values()): + break + return done, number_of_users_in_current_dispatch + + # TODO: test def all_users_have_been_dispatched( dispatched_users: Dict[str, Dict[str, int]], @@ -146,7 +161,7 @@ def all_users_have_been_dispatched( ) -> bool: return all( sum(x[user_class] for x in dispatched_users.values()) - > sum(x[user_class] for x in effective_balanced_users.values()) + >= sum(x[user_class] for x in effective_balanced_users.values()) for user_class in user_class_occurrences.keys() ) @@ -159,7 +174,7 @@ def all_users_of_current_class_have_been_dispatched( ) -> bool: return ( sum(x[user_class] for x in dispatched_users.values()) - > sum(x[user_class] for x in effective_balanced_users.values()) + >= sum(x[user_class] for x in effective_balanced_users.values()) ) @@ -190,18 +205,6 @@ def add_dispatched_users( } -def current_dispatch_ready( - balanced_users: Dict[str, Dict[str, int]], - dispatched_users: Dict[str, Dict[str, int]], - number_of_users_each_dispatch: int, -) -> bool: - if sum(sum(x.values()) for x in dispatched_users.values()) == number_of_users_each_dispatch: - return True - if sum(sum(x.values()) for x in balanced_users.values()) == 0: - return True - return False - - def balance_users_among_workers( worker_nodes: List[WorkerNode], user_class_occurrences: Dict[str, int], diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 23dc1e5ad3..6229ca0012 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -105,7 +105,7 @@ def test_balance_users_among_3_workers(self): self.assertDictEqual(balanced_users, expected_balanced_users) -class TestDispatchUsersWithWorkersWithoutUsers(unittest.TestCase): +class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): maxDiff = None def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): @@ -773,7 +773,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): self.assertRaises(StopIteration, lambda: next(users_dispatcher)) -class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): +class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): maxDiff = None def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): @@ -972,6 +972,36 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): self.assertRaises(StopIteration, lambda: next(users_dispatcher)) +class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): + maxDiff = None + + def test_dispatch_users_to_3_workers(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {"User3": 15} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + for spawn_rate in [0.5, 1, 2, 3, 4, 9]: + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=spawn_rate, + ) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + + class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): maxDiff = None From 7d54784ced81aff1f5b120af0f24539c813e0244 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 13 Nov 2020 12:10:55 -0500 Subject: [PATCH 004/139] Code cleanup --- locust/dispatch.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 5eca3bee11..d97faaeb70 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -83,10 +83,7 @@ def dispatch_users( gevent.sleep(max(0.0, wait_between_dispatch - delta)) else: - while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - done = False - if all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): - break + while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): number_of_users_in_current_dispatch = 0 for user_class in user_class_occurrences.keys(): if all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, user_class): @@ -101,12 +98,6 @@ def dispatch_users( if done: break - if not done: - # We have no more users to dispatch and - # number_of_users_in_current_dispatch < number_of_users_per_dispatch, - # thus we need to break out of the while loop - break - if all( sum(x[user_class] for x in dispatched_users.values()) >= sum(x[user_class] for x in balanced_users.values()) From 408f3eddc18182e3e35c4d14c6e780744116bdd2 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 13 Nov 2020 12:44:03 -0500 Subject: [PATCH 005/139] Code cleanup/refactoring --- locust/dispatch.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index d97faaeb70..5518dd465d 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -82,6 +82,12 @@ def dispatch_users( delta = time.time() - ts gevent.sleep(max(0.0, wait_between_dispatch - delta)) + elif ( + not less_users_than_desired + and number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) <= number_of_users_per_dispatch + ): + yield balanced_users + else: while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): number_of_users_in_current_dispatch = 0 @@ -98,14 +104,6 @@ def dispatch_users( if done: break - if all( - sum(x[user_class] for x in dispatched_users.values()) - >= sum(x[user_class] for x in balanced_users.values()) - for user_class in user_class_occurrences.keys() - ): - # TODO: Explain - break - ts = time.time() yield { worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) @@ -122,6 +120,22 @@ def dispatch_users( yield balanced_users +# TODO: test +def number_of_users_left_to_dispatch( + dispatched_users: Dict[str, Dict[str, int]], + balanced_users: Dict[str, Dict[str, int]], + user_class_occurrences: Dict[str, int], +) -> int: + return sum( + max( + 0, + sum(x[user_class] for x in balanced_users.values()) + - sum(x[user_class] for x in dispatched_users.values()) + ) + for user_class in user_class_occurrences.keys() + ) + + def distribute_current_user_class_among_workers( dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], From 3b8167d1f7e4f0e7fab014c6ceec372dab384bf8 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 12:49:45 -0500 Subject: [PATCH 006/139] Update dispatch logic --- locust/dispatch.py | 34 ++- locust/distribution.py | 3 + locust/test/test_dispatch.py | 454 ++++++++++++++++++++++++++++++- locust/test/test_distribution.py | 13 + 4 files changed, 488 insertions(+), 16 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 5518dd465d..0e36b0bb35 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import math import time @@ -6,11 +8,13 @@ Dict, Generator, List, + TYPE_CHECKING, ) import gevent -from locust.runners import WorkerNode +if TYPE_CHECKING: + from locust.runners import WorkerNode def dispatch_users( @@ -44,18 +48,18 @@ def dispatch_users( for worker_node in worker_nodes } - wait_between_dispatch = math.ceil(1 / spawn_rate) + number_of_users_per_dispatch = max(1, math.floor(spawn_rate)) - number_of_users_per_dispatch = math.ceil(spawn_rate) + wait_between_dispatch = number_of_users_per_dispatch / spawn_rate dispatched_users = deepcopy(initial_dispatched_users) # The amount of users in each user class # is less than the desired amount less_users_than_desired = all( - sum(x[user_class] for x in dispatched_users.values()) - < sum(x[user_class] for x in effective_balanced_users.values()) - for user_class in user_class_occurrences.keys() + sum(x[user_class] for x in dispatched_users.values()) + < sum(x[user_class] for x in effective_balanced_users.values()) + for user_class in user_class_occurrences.keys() ) if less_users_than_desired: @@ -79,8 +83,9 @@ def dispatch_users( worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) } - delta = time.time() - ts - gevent.sleep(max(0.0, wait_between_dispatch - delta)) + if sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: + delta = time.time() - ts + gevent.sleep(max(0.0, wait_between_dispatch - delta)) elif ( not less_users_than_desired @@ -109,8 +114,9 @@ def dispatch_users( worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) } - delta = time.time() - ts - gevent.sleep(max(0.0, wait_between_dispatch - delta)) + if not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): + delta = time.time() - ts + gevent.sleep(max(0.0, wait_between_dispatch - delta)) # If we are here, it means we have an excess of users for one or more user classes. # Hence, we need to dispatch a last set of users that will bring the desired users @@ -178,8 +184,8 @@ def all_users_of_current_class_have_been_dispatched( user_class: str, ) -> bool: return ( - sum(x[user_class] for x in dispatched_users.values()) - >= sum(x[user_class] for x in effective_balanced_users.values()) + sum(x[user_class] for x in dispatched_users.values()) + >= sum(x[user_class] for x in effective_balanced_users.values()) ) @@ -201,8 +207,8 @@ def add_dispatched_users( return { worker_node_id: { user_class: ( - dispatched_users1.get(worker_node_id, {}).get(user_class, 0) - + dispatched_users2.get(worker_node_id, {}).get(user_class, 0) + dispatched_users1.get(worker_node_id, {}).get(user_class, 0) + + dispatched_users2.get(worker_node_id, {}).get(user_class, 0) ) for user_class in user_classes } diff --git a/locust/distribution.py b/locust/distribution.py index 2a866e02b9..c74065bef2 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -22,6 +22,9 @@ def weight_users( """ assert number_of_users >= 0 + if len(user_classes) == 0: + return {} + user_classes = sorted(user_classes, key=lambda u: u.__name__) user_class_occurrences = {user_class.__name__: 0 for user_class in user_classes} diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 6229ca0012..061e276892 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -108,6 +108,105 @@ def test_balance_users_among_3_workers(self): class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): maxDiff = None + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.15, + ) + + sleep_time = 1 / 0.15 + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") worker_node2 = WorkerNode("2") @@ -200,7 +299,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -294,7 +396,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -352,7 +457,73 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2.4, + ) + + sleep_time = 2 / 2.4 + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -392,7 +563,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -432,7 +606,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -454,12 +631,99 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): maxDiff = None + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.15, + ) + + sleep_time = 1 / 0.15 + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") worker_node1.user_class_occurrences = {} @@ -537,7 +801,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -616,7 +883,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -668,7 +938,67 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 1} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 1} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2.4, + ) + + sleep_time = 2 / 2.4 + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -711,7 +1041,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -745,7 +1078,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -770,12 +1106,63 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): maxDiff = None + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.15, + ) + + sleep_time = 1 / 0.15 + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") worker_node1.user_class_occurrences = {} @@ -817,7 +1204,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -860,7 +1250,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -894,7 +1287,49 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_class_occurrences = {} + worker_node2 = WorkerNode("2") + worker_node2.user_class_occurrences = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_class_occurrences = {"User2": 7} + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2.4, + ) + + sleep_time = 2 / 2.4 + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) + + ts = time.time() + self.assertDictEqual(next(users_dispatcher), { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -919,7 +1354,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -944,7 +1382,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -969,7 +1410,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): @@ -983,7 +1427,7 @@ def test_dispatch_users_to_3_workers(self): worker_node3 = WorkerNode("3") worker_node3.user_class_occurrences = {"User2": 7} - for spawn_rate in [0.5, 1, 2, 3, 4, 9]: + for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, @@ -999,7 +1443,10 @@ def test_dispatch_users_to_3_workers(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): @@ -1013,7 +1460,7 @@ def test_dispatch_users_to_3_workers(self): worker_node3 = WorkerNode("3") worker_node3.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} - for spawn_rate in [0.5, 1, 2, 3, 4, 9]: + for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, @@ -1029,4 +1476,7 @@ def test_dispatch_users_to_3_workers(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, delta) diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 5a8209f9cf..73f9c500d4 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -7,6 +7,19 @@ class TestDistribution(unittest.TestCase): maxDiff = None + def test_distribution_no_user_classes(self): + user_class_occurrences = weight_users( + user_classes=[], + number_of_users=0, + ) + self.assertDictEqual(user_class_occurrences, {}) + + user_class_occurrences = weight_users( + user_classes=[], + number_of_users=1, + ) + self.assertDictEqual(user_class_occurrences, {}) + def test_distribution_equal_weights_and_fewer_amount_than_user_classes(self): class User1(User): weight = 1 From 5ecf8b19611784c6ee5811d8782e83186984104d Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 13:47:28 -0500 Subject: [PATCH 007/139] Cleanup some imports --- locust/test/test_client.py | 1 - locust/test/test_locust_class.py | 2 +- locust/test/test_log.py | 1 - locust/test/test_web.py | 2 -- locust/test/test_zmqrpc.py | 1 - locust/test/testcases.py | 1 - 6 files changed, 1 insertion(+), 7 deletions(-) diff --git a/locust/test/test_client.py b/locust/test/test_client.py index 46d00d7a2e..10da011fa0 100644 --- a/locust/test/test_client.py +++ b/locust/test/test_client.py @@ -2,7 +2,6 @@ from locust.clients import HttpSession -from locust.env import Environment from locust.exception import ResponseError from .testcases import WebserverTestCase diff --git a/locust/test/test_locust_class.py b/locust/test/test_locust_class.py index 27e2e9dbd3..4ac16c708b 100644 --- a/locust/test/test_locust_class.py +++ b/locust/test/test_locust_class.py @@ -5,7 +5,7 @@ from locust.exception import InterruptTaskSet, ResponseError from locust import HttpUser, User, TaskSet, task, constant from locust.env import Environment -from locust.exception import CatchResponseError, LocustError, RescheduleTask, RescheduleTaskImmediately, StopUser +from locust.exception import CatchResponseError, RescheduleTask, RescheduleTaskImmediately, StopUser from .testcases import LocustTestCase, WebserverTestCase diff --git a/locust/test/test_log.py b/locust/test/test_log.py index 42a6ba3eb1..1548e3fda2 100644 --- a/locust/test/test_log.py +++ b/locust/test/test_log.py @@ -1,5 +1,4 @@ import mock -import os import socket import subprocess import textwrap diff --git a/locust/test/test_web.py b/locust/test/test_web.py index b6c522c8bc..86c1ba2a70 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -2,9 +2,7 @@ import csv import json import os -import sys import traceback -from datetime import datetime, timedelta from io import StringIO from tempfile import NamedTemporaryFile diff --git a/locust/test/test_zmqrpc.py b/locust/test/test_zmqrpc.py index e32e0b2626..4b9d9428f9 100644 --- a/locust/test/test_zmqrpc.py +++ b/locust/test/test_zmqrpc.py @@ -1,4 +1,3 @@ -import unittest from time import sleep import zmq from locust.rpc import zmqrpc, Message diff --git a/locust/test/testcases.py b/locust/test/testcases.py index faabd7b91e..1ec9f00d3a 100644 --- a/locust/test/testcases.py +++ b/locust/test/testcases.py @@ -4,7 +4,6 @@ import sys import unittest import warnings -from copy import copy from io import BytesIO import gevent From 41a242ddae0e38280cf1277aee54e8e5037a9b47 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 13:48:40 -0500 Subject: [PATCH 008/139] type hint compatible with python 3.6 --- locust/dispatch.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 0e36b0bb35..2eacb3a0eb 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import itertools import math import time @@ -18,7 +16,7 @@ def dispatch_users( - worker_nodes: List[WorkerNode], + worker_nodes, # type: List[WorkerNode] user_class_occurrences: Dict[str, int], spawn_rate: float, ) -> Generator[Dict[str, Dict[str, int]], None, None]: @@ -217,7 +215,7 @@ def add_dispatched_users( def balance_users_among_workers( - worker_nodes: List[WorkerNode], + worker_nodes, # type: List[WorkerNode] user_class_occurrences: Dict[str, int], ) -> Dict[str, Dict[str, int]]: balanced_users = { From 01e8b7c7795f1836e5395b933d377188caa76904 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 13:49:08 -0500 Subject: [PATCH 009/139] Fix sleep in dispatch logic --- locust/dispatch.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 2eacb3a0eb..6fcfe019b3 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -112,9 +112,8 @@ def dispatch_users( worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) } - if not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): - delta = time.time() - ts - gevent.sleep(max(0.0, wait_between_dispatch - delta)) + delta = time.time() - ts + gevent.sleep(max(0.0, wait_between_dispatch - delta)) # If we are here, it means we have an excess of users for one or more user classes. # Hence, we need to dispatch a last set of users that will bring the desired users From d9bb87f1680ee491b82dabba2a03b9f1b0a70293 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 14:25:52 -0500 Subject: [PATCH 010/139] Refactoring for handling new users distribution and dispatch logic --- locust/env.py | 30 ++- locust/runners.py | 455 ++++++++++++++++++++++-------------- locust/test/test_runners.py | 412 +++++++++++++++++++------------- locust/test/test_stats.py | 34 +-- locust/user/users.py | 8 + 5 files changed, 575 insertions(+), 364 deletions(-) diff --git a/locust/env.py b/locust/env.py index f3cbecb5e4..3aaa98da2c 100644 --- a/locust/env.py +++ b/locust/env.py @@ -6,7 +6,12 @@ from .user import User from .user.task import filter_tasks_by_tags from .shape import LoadTestShape -from typing import List +from typing import ( + Dict, + List, + Type, + Union, +) class Environment: @@ -16,7 +21,7 @@ class Environment: See :ref:`events` for available events. """ - user_classes: List[User] = [] + user_classes: List[Type[User]] = [] """User classes that the runner will run""" shape_class: LoadTestShape = None @@ -66,7 +71,7 @@ class Environment: def __init__( self, *, - user_classes=[], + user_classes=None, shape_class=None, tags=None, exclude_tags=None, @@ -82,7 +87,7 @@ def __init__( else: self.events = Events() - self.user_classes = user_classes + self.user_classes = user_classes or [] self.shape_class = shape_class self.tags = tags self.exclude_tags = exclude_tags @@ -95,19 +100,21 @@ def __init__( self._filter_tasks_by_tags() - def _create_runner(self, runner_class, *args, **kwargs): + def _create_runner( + self, runner_class: Union[Type[LocalRunner], Type[MasterRunner], Type[WorkerRunner]], *args, **kwargs, + ) -> Union[LocalRunner, MasterRunner, WorkerRunner]: if self.runner is not None: raise RunnerAlreadyExistsError("Environment.runner already exists (%s)" % self.runner) - self.runner = runner_class(self, *args, **kwargs) + self.runner: Union[LocalRunner, MasterRunner, WorkerRunner] = runner_class(self, *args, **kwargs) return self.runner - def create_local_runner(self): + def create_local_runner(self) -> LocalRunner: """ Create a :class:`LocalRunner ` instance for this Environment """ return self._create_runner(LocalRunner) - def create_master_runner(self, master_bind_host="*", master_bind_port=5557): + def create_master_runner(self, master_bind_host="*", master_bind_port=5557) -> MasterRunner: """ Create a :class:`MasterRunner ` instance for this Environment @@ -121,7 +128,7 @@ def create_master_runner(self, master_bind_host="*", master_bind_port=5557): master_bind_port=master_bind_port, ) - def create_worker_runner(self, master_host, master_port): + def create_worker_runner(self, master_host, master_port) -> WorkerRunner: """ Create a :class:`WorkerRunner ` instance for this Environment @@ -186,3 +193,8 @@ def _filter_tasks_by_tags(self): for user_class in self.user_classes: filter_tasks_by_tags(user_class, self.tags, self.exclude_tags) + + # TODO: Test this + @property + def user_classes_by_name(self) -> Dict[str, Type[User]]: + return {u.__name__: u for u in self.user_classes} diff --git a/locust/runners.py b/locust/runners.py index e139ef2df5..43bfe4c375 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -1,12 +1,18 @@ # -*- coding: utf-8 -*- +import json import logging -import random import socket import sys +import time import traceback -import warnings +from collections.abc import MutableMapping +from typing import ( + Dict, + Iterator, + List, + ValuesView, +) from uuid import uuid4 -from time import time import gevent import greenlet @@ -14,13 +20,18 @@ from gevent.pool import Group from . import User -from .log import greenlet_exception_logger -from .rpc import Message, rpc -from .stats import RequestStats, setup_distributed_stats_event_listeners - +from .dispatch import dispatch_users +from .distribution import weight_users from .exception import RPCError -from .user.task import LOCUST_STATE_STOPPING - +from .log import greenlet_exception_logger +from .rpc import ( + Message, + rpc, +) +from .stats import ( + RequestStats, + setup_distributed_stats_event_listeners, +) logger = logging.getLogger(__name__) @@ -65,6 +76,7 @@ def __init__(self, environment): self.shape_last_state = None self.current_cpu_usage = 0 self.cpu_warning_emitted = False + self.worker_cpu_warning_emitted = False self.greenlet.spawn(self.monitor_cpu).link_exception(greenlet_exception_handler) self.exceptions = {} self.target_user_count = None @@ -99,6 +111,10 @@ def __del__(self): def user_classes(self): return self.environment.user_classes + @property + def user_classes_by_name(self): + return self.environment.user_classes_by_name + @property def stats(self) -> RequestStats: return self.environment.stats @@ -114,6 +130,18 @@ def user_count(self): """ return len(self.user_greenlets) + # TODO: Test this + @property + def user_class_occurrences(self) -> Dict[str, int]: + """ + :returns: Number of currently running users for each user class + """ + user_class_occurrences = {user_class.__name__: 0 for user_class in self.user_classes} + for user_greenlet in self.user_greenlets: + user = user_greenlet.args[0] + user_class_occurrences[user.__class__.__name__] += 1 + return user_class_occurrences + def update_state(self, new_state): """ Updates the current state @@ -132,105 +160,66 @@ def cpu_log_warning(self): return True return False - def weight_users(self, amount): - """ - Distributes the amount of users for each WebLocust-class according to it's weight - returns a list "bucket" with the weighted users - """ - for user in self.user_classes: - if self.environment.host is not None: - user.host = self.environment.host - weights = [user.weight for user in self.user_classes] - bucket = random.choices(self.user_classes, weights=weights, k=amount) - return bucket - - def spawn_users(self, spawn_count, spawn_rate, wait=False): - bucket = self.weight_users(spawn_count) - spawn_count = len(bucket) + def spawn_users(self, user_classes_spawn_count: Dict[str, int], wait: bool = False): if self.state == STATE_INIT or self.state == STATE_STOPPED: self.update_state(STATE_SPAWNING) - existing_count = len(self.user_greenlets) logger.info( - "Spawning %i users at the rate %g users/s (%i users already running)..." - % (spawn_count, spawn_rate, existing_count) + "Spawning additional %s (%s already running)..." + % (json.dumps(user_classes_spawn_count), json.dumps(self.user_class_occurrences)) ) - occurrence_count = dict([(l.__name__, 0) for l in self.user_classes]) - def spawn(): - sleep_time = 1.0 / spawn_rate - while True: - if not bucket: - logger.info( - "All users spawned: %s (%i total running)" - % ( - ", ".join(["%s: %d" % (name, count) for name, count in occurrence_count.items()]), - len(self.user_greenlets), - ) - ) - self.environment.events.spawning_complete.fire(user_count=len(self.user_greenlets)) - return - - user_class = bucket.pop() - occurrence_count[user_class.__name__] += 1 - new_user = user_class(self.environment) + def spawn(user_class: str, spawn_count: int): + n = 0 + while n < spawn_count: + new_user = self.user_classes_by_name[user_class](self.environment) + if self.environment.host is not None: + new_user.host = self.environment.host new_user.start(self.user_greenlets) - if len(self.user_greenlets) % 10 == 0: - logger.debug("%i users spawned" % len(self.user_greenlets)) - if bucket: - gevent.sleep(sleep_time) + n += 1 + if n % 10 == 0 or n == spawn_count: + logger.debug("%i users spawned" % self.user_count) + logger.info("All users of class %s spawned" % user_class) + + for user_class, spawn_count in user_classes_spawn_count.items(): + spawn(user_class, spawn_count) - spawn() if wait: self.user_greenlets.join() logger.info("All users stopped\n") - def stop_users(self, user_count, stop_rate=None): - """ - Stop `user_count` weighted users at a rate of `stop_rate` - """ - if user_count == 0 or stop_rate == 0: - return + def stop_users(self, user_classes_stop_count: Dict[str, int]): + async_calls_to_stop = Group() + stop_group = Group() - bucket = self.weight_users(user_count) - user_count = len(bucket) - to_stop = [] - for g in self.user_greenlets: - for l in bucket: + for user_class, stop_count in user_classes_stop_count.items(): + if self.user_class_occurrences[user_class] == 0: + continue + + to_stop = [] + for g in self.user_greenlets: + if len(to_stop) == stop_count: + break user = g.args[0] - if isinstance(user, l): + if isinstance(user, self.user_classes_by_name[user_class]): to_stop.append(user) - bucket.remove(l) - break - - if not to_stop: - return - - if stop_rate is None or stop_rate >= user_count: - sleep_time = 0 - logger.info("Stopping %i users" % (user_count)) - else: - sleep_time = 1.0 / stop_rate - logger.info("Stopping %i users at rate of %g users/s" % (user_count, stop_rate)) - async_calls_to_stop = Group() - stop_group = Group() + if not to_stop: + continue - while True: - user_to_stop: User = to_stop.pop() - logger.debug("Stopping %s" % user_to_stop._greenlet.name) - if user_to_stop._greenlet is greenlet.getcurrent(): - # User called runner.quit(), so dont block waiting for killing to finish" - user_to_stop._group.killone(user_to_stop._greenlet, block=False) - elif self.environment.stop_timeout: - async_calls_to_stop.add(gevent.spawn_later(0, User.stop, user_to_stop, force=False)) - stop_group.add(user_to_stop._greenlet) - else: - async_calls_to_stop.add(gevent.spawn_later(0, User.stop, user_to_stop, force=True)) - if to_stop: - gevent.sleep(sleep_time) - else: - break + while True: + user_to_stop: User = to_stop.pop() + logger.debug("Stopping %s" % user_to_stop.greenlet.name) + if user_to_stop.greenlet is greenlet.getcurrent(): + # User called runner.quit(), so dont block waiting for killing to finish" + user_to_stop.group.killone(user_to_stop.greenlet, block=False) + elif self.environment.stop_timeout: + async_calls_to_stop.add(gevent.spawn_later(0, User.stop, user_to_stop, force=False)) + stop_group.add(user_to_stop.greenlet) + else: + async_calls_to_stop.add(gevent.spawn_later(0, User.stop, user_to_stop, force=True)) + if not to_stop: + break async_calls_to_stop.join() @@ -241,7 +230,7 @@ def stop_users(self, user_count, stop_rate=None): ) stop_group.kill(block=True) - logger.info("%i Users have been stopped, %g still running" % (user_count, len(self.user_greenlets))) + logger.info("%i Users have been stopped, %g still running" % (sum(user_classes_stop_count.values()), self.user_count)) def monitor_cpu(self): process = psutil.Process() @@ -254,7 +243,7 @@ def monitor_cpu(self): self.cpu_warning_emitted = True gevent.sleep(CPU_MONITOR_INTERVAL) - def start(self, user_count, spawn_rate, wait=False): + def start(self, user_count: int, spawn_rate: float, wait: bool = False): """ Start running a load test @@ -269,26 +258,46 @@ def start(self, user_count, spawn_rate, wait=False): self.exceptions = {} self.cpu_warning_emitted = False self.worker_cpu_warning_emitted = False - self.target_user_count = user_count + + if wait and user_count - self.user_count > spawn_rate: + raise ValueError("wait is True but the amount of users to add is greater than the spawn rate") + + users_dispatcher = dispatch_users( + worker_nodes=[WorkerNode(id="dummy")], + user_class_occurrences=weight_users(self.user_classes, user_count), + spawn_rate=spawn_rate, + ) if self.state != STATE_INIT and self.state != STATE_STOPPED: logger.debug( "Updating running test with %d users, %.2f spawn rate and wait=%r" % (user_count, spawn_rate, wait) ) self.update_state(STATE_SPAWNING) - if self.user_count > user_count: - # Stop some users - stop_count = self.user_count - user_count - self.stop_users(stop_count, spawn_rate) - elif self.user_count < user_count: - # Spawn some users - spawn_count = user_count - self.user_count - self.spawn_users(spawn_count=spawn_count, spawn_rate=spawn_rate) + + for dispatched_users in users_dispatcher: + user_classes_spawn_count = {} + user_classes_stop_count = {} + user_class_occurrences = dispatched_users["dummy"] + for user_class, occurrences in user_class_occurrences.items(): + logger.debug( + "Updating running test with %d users of class %s and wait=%r" % (occurrences, user_class, wait) + ) + if self.user_class_occurrences[user_class] > occurrences: + user_classes_stop_count[user_class] = self.user_class_occurrences[user_class] - occurrences + elif self.user_class_occurrences[user_class] < occurrences: + user_classes_spawn_count[user_class] = occurrences - self.user_class_occurrences[user_class] + + if wait: + # spawn_users will block, so we need to call stop_users first + self.stop_users(user_classes_stop_count) + self.spawn_users(user_classes_spawn_count, wait) else: - self.environment.events.spawning_complete.fire(user_count=self.user_count) - else: - self.spawn_rate = spawn_rate - self.spawn_users(user_count, spawn_rate=spawn_rate, wait=wait) + # call spawn_users before stopping the users since stop_users + # can be blocking because of the stop_timeout + self.spawn_users(user_classes_spawn_count, wait) + self.stop_users(user_classes_stop_count) + + self.environment.events.spawning_complete.fire(user_count=self.user_count) def start_shape(self): if self.shape_greenlet: @@ -311,6 +320,9 @@ def shape_worker(self): self.quit() else: self.stop() + self.shape_greenlet = None + self.shape_last_state = None + return elif self.shape_last_state == new_state: gevent.sleep(1) else: @@ -325,11 +337,24 @@ def stop(self): """ logger.debug("Stopping all users") self.update_state(STATE_CLEANUP) + # if we are currently spawning users we need to kill the spawning greenlet first if self.spawning_greenlet and not self.spawning_greenlet.ready(): self.spawning_greenlet.kill(block=True) - self.stop_users(self.user_count) + + if self.environment.shape_class is not None and self.shape_greenlet is not greenlet.getcurrent(): + self.shape_greenlet.kill(block=True) + self.shape_greenlet = None + self.shape_last_state = None + + user_classes_stop_count = { + user_class: self.user_class_occurrences[user_class] + for user_class, occurrences in self.user_class_occurrences.items() + } + self.stop_users(user_classes_stop_count) + self.update_state(STATE_STOPPED) + self.cpu_log_warning() def quit(self): @@ -365,8 +390,7 @@ def on_user_error(user_instance, exception, tb): self.environment.events.user_error.add_listener(on_user_error) - def start(self, user_count, spawn_rate, wait=False): - self.target_user_count = user_count + def start(self, user_count: int, spawn_rate: float, wait: bool = False): if spawn_rate > 100: logger.warning( "Your selected spawn rate is very high (>100), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" @@ -398,13 +422,60 @@ def __init__(self, *args, **kwargs): class WorkerNode: - def __init__(self, id, state=STATE_INIT, heartbeat_liveness=HEARTBEAT_LIVENESS): - self.id = id + def __init__(self, id: str, state=STATE_INIT, heartbeat_liveness=HEARTBEAT_LIVENESS): + self.id: str = id self.state = state - self.user_count = 0 self.heartbeat = heartbeat_liveness self.cpu_usage = 0 self.cpu_warning_emitted = False + self.user_class_occurrences: Dict[str, int] = {} + + @property + def user_count(self) -> int: + return sum(self.user_class_occurrences.values()) + + +class WorkerNodes(MutableMapping): + def __init__(self): + self._worker_nodes = {} + + def get_by_state(self, state) -> List[WorkerNode]: + return [c for c in self.values() if c.state == state] + + @property + def all(self) -> ValuesView[WorkerNode]: + return self.values() + + @property + def ready(self) -> List[WorkerNode]: + return self.get_by_state(STATE_INIT) + + @property + def spawning(self) -> List[WorkerNode]: + return self.get_by_state(STATE_SPAWNING) + + @property + def running(self) -> List[WorkerNode]: + return self.get_by_state(STATE_RUNNING) + + @property + def missing(self) -> List[WorkerNode]: + return self.get_by_state(STATE_MISSING) + + def __setitem__(self, k: str, v: WorkerNode) -> None: + self._worker_nodes[k] = v + + def __delitem__(self, k: str) -> None: + del self._worker_nodes[k] + + def __getitem__(self, k: str) -> WorkerNode: + return self._worker_nodes[k] + + def __len__(self) -> int: + return len(self._worker_nodes) + + def __iter__(self) -> Iterator[WorkerNode]: + return iter(self._worker_nodes) class MasterRunner(DistributedRunner): @@ -427,32 +498,10 @@ def __init__(self, environment, master_bind_host, master_bind_port): self.worker_cpu_warning_emitted = False self.master_bind_host = master_bind_host self.master_bind_port = master_bind_port + self.target_user_class_occurrences: Dict[str, int] = {} + self.spawn_rate: float = 0 - class WorkerNodesDict(dict): - def get_by_state(self, state): - return [c for c in self.values() if c.state == state] - - @property - def all(self): - return self.values() - - @property - def ready(self): - return self.get_by_state(STATE_INIT) - - @property - def spawning(self): - return self.get_by_state(STATE_SPAWNING) - - @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.clients = WorkerNodes() try: self.server = rpc.Server(master_bind_host, master_bind_port) except RPCError as e: @@ -473,8 +522,7 @@ def on_worker_report(client_id, data): if client_id not in self.clients: logger.info("Discarded report from unrecognized worker %s", client_id) return - - self.clients[client_id].user_count = data["user_count"] + self.clients[client_id].user_class_occurrences = data["user_class_occurrences"] self.environment.events.worker_report.add_listener(on_worker_report) @@ -486,7 +534,7 @@ def on_quitting(environment, **kw): @property def user_count(self): - return sum([c.user_count for c in self.clients.values()]) + return sum(c.user_count for c in self.clients.values()) def cpu_log_warning(self): warning_emitted = Runner.cpu_log_warning(self) @@ -495,8 +543,7 @@ def cpu_log_warning(self): warning_emitted = True return warning_emitted - def start(self, user_count, spawn_rate): - self.target_user_count = user_count + def start(self, user_count: int, spawn_rate: float, **kwargs): num_workers = len(self.clients.ready) + len(self.clients.running) + len(self.clients.spawning) if not num_workers: logger.warning( @@ -505,14 +552,14 @@ def start(self, user_count, spawn_rate): ) return + self.target_user_class_occurrences = weight_users(self.user_classes, user_count) + self.spawn_rate = spawn_rate - worker_num_users = user_count // (num_workers or 1) - worker_spawn_rate = float(spawn_rate) / (num_workers or 1) - remaining = user_count % num_workers + worker_spawn_rate = float(spawn_rate) / (num_workers or 1) logger.info( "Sending spawn jobs of %d users and %.2f spawn rate to %d ready clients" - % (worker_num_users, worker_spawn_rate, num_workers) + % (user_count, worker_spawn_rate, num_workers) ) if worker_spawn_rate > 100: @@ -525,20 +572,28 @@ def start(self, user_count, spawn_rate): self.exceptions = {} self.environment.events.test_start.fire(environment=self.environment) - for client in self.clients.ready + self.clients.running + self.clients.spawning: - data = { - "spawn_rate": worker_spawn_rate, - "num_users": worker_num_users, - "host": self.environment.host, - "stop_timeout": self.environment.stop_timeout, - } - - if remaining > 0: - data["num_users"] += 1 - remaining -= 1 + users_dispatcher = dispatch_users( + worker_nodes=self.clients.ready + self.clients.running + self.clients.spawning, + user_class_occurrences=self.target_user_class_occurrences, + spawn_rate=spawn_rate, + ) - logger.debug("Sending spawn message to client %s" % (client.id)) - self.server.send_to_client(Message("spawn", data, client.id)) + for dispatched_users in users_dispatcher: + dispatch_greenlets = Group() + for worker_node_id, worker_user_class_occurrences in dispatched_users.items(): + data = { + "timestamp": time.time(), + "user_class_occurrences": worker_user_class_occurrences, + "host": self.environment.host, + "stop_timeout": self.environment.stop_timeout, + } + dispatch_greenlets.add(gevent.spawn_later( + 0, + self.server.send_to_client, + Message("spawn", data, worker_node_id), + )) + logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) + dispatch_greenlets.join() self.update_state(STATE_SPAWNING) @@ -547,13 +602,26 @@ def stop(self): logger.debug("Stopping...") self.update_state(STATE_STOPPING) - if self.environment.shape_class: + if self.environment.shape_class is not None and self.shape_greenlet is not greenlet.getcurrent(): + self.shape_greenlet.kill(block=True) + self.shape_greenlet = None self.shape_last_state = None for client in self.clients.all: - logger.debug("Sending stop message to client %s" % (client.id)) + logger.debug("Sending stop message to client %s" % client.id) self.server.send_to_client(Message("stop", None, client.id)) + # Give 60s more for all workers to stop + timeout = gevent.Timeout(self.environment.stop_timeout or 0 + 60) + timeout.start() + try: + while self.user_count != 0: + gevent.sleep(1) + except gevent.Timeout: + logger.error("Timeout waiting for all workers to stop") + finally: + timeout.cancel() + self.environment.events.test_stop.fire(environment=self.environment) def quit(self): @@ -584,7 +652,7 @@ def heartbeat_worker(self): if client.heartbeat < 0 and client.state != STATE_MISSING: logger.info("Worker %s failed to send heartbeat, setting state to missing." % str(client.id)) client.state = STATE_MISSING - client.user_count = 0 + client.user_class_occurrences = {} if self.worker_count <= 0: logger.info("The last worker went missing, stopping test.") self.stop() @@ -612,11 +680,11 @@ def client_listener(self): self.connection_broken = False msg.node_id = client_id if msg.type == "client_ready": - id = msg.node_id - self.clients[id] = WorkerNode(id, heartbeat_liveness=HEARTBEAT_LIVENESS) + worker_node_id = msg.node_id + self.clients[worker_node_id] = WorkerNode(worker_node_id, heartbeat_liveness=HEARTBEAT_LIVENESS) logger.info( "Client %r reported as ready. Currently %i clients ready to swarm." - % (id, len(self.clients.ready + self.clients.running + self.clients.spawning)) + % (worker_node_id, len(self.clients.ready + self.clients.running + self.clients.spawning)) ) if self.state == STATE_RUNNING or self.state == STATE_SPAWNING: # balance the load distribution when new client joins @@ -653,10 +721,9 @@ def client_listener(self): self.clients[msg.node_id].state = STATE_SPAWNING elif msg.type == "spawning_complete": self.clients[msg.node_id].state = STATE_RUNNING - self.clients[msg.node_id].user_count = msg.data["count"] + self.clients[msg.node_id].user_class_occurrences = msg.data["user_class_occurrences"] if len(self.clients.spawning) == 0: - count = sum(c.user_count for c in self.clients.values()) - self.environment.events.spawning_complete.fire(user_count=count) + self.environment.events.spawning_complete.fire(user_count=self.user_count) elif msg.type == "quit": if msg.node_id in self.clients: del self.clients[msg.node_id] @@ -677,6 +744,11 @@ def client_listener(self): def worker_count(self): return len(self.clients.ready) + len(self.clients.spawning) + len(self.clients.running) + # TODO: Test this + @property + def target_user_count(self) -> int: + return sum(self.user_class_occurrences.values()) + class WorkerRunner(DistributedRunner): """ @@ -698,6 +770,7 @@ def __init__(self, environment, master_host, master_port): self.client_id = socket.gethostname() + "_" + uuid4().hex self.master_host = master_host self.master_port = master_port + self.worker_cpu_warning_emitted = False self.client = rpc.Client(master_host, master_port, self.client_id) self.greenlet.spawn(self.heartbeat).link_exception(greenlet_exception_handler) self.greenlet.spawn(self.worker).link_exception(greenlet_exception_handler) @@ -705,15 +778,15 @@ def __init__(self, environment, master_host, master_port): self.greenlet.spawn(self.stats_reporter).link_exception(greenlet_exception_handler) # register listener for when all users have spawned, and report it to the master node - def on_spawning_complete(user_count): - self.client.send(Message("spawning_complete", {"count": user_count}, self.client_id)) + def on_spawning_complete(user_class_occurrences): + self.client.send(Message("spawning_complete", {"user_class_occurrences": user_class_occurrences}, self.client_id)) self.worker_state = STATE_RUNNING self.environment.events.spawning_complete.add_listener(on_spawning_complete) # register listener that adds the current number of spawned users to the report that is sent to the master node def on_report_to_master(client_id, data): - data["user_count"] = self.user_count + data["user_class_occurrences"] = self.user_class_occurrences self.environment.events.report_to_master.add_listener(on_report_to_master) @@ -730,6 +803,37 @@ def on_user_error(user_instance, exception, tb): self.environment.events.user_error.add_listener(on_user_error) + def start(self, user_count, spawn_rate, wait=False): + raise NotImplementedError("user start_worker") + + def start_worker(self, user_class_occurrences: Dict[str, int], **kwargs): + """ + Start running a load test as a worker + + :param user_class_occurrences: Users to run + """ + if self.worker_state != STATE_RUNNING and self.worker_state != STATE_SPAWNING: + self.stats.clear_all() + self.exceptions = {} + self.cpu_warning_emitted = False + self.worker_cpu_warning_emitted = False + + user_classes_spawn_count = {} + user_classes_stop_count = {} + + for user_class, occurrences in user_class_occurrences.items(): + if self.user_class_occurrences[user_class] > occurrences: + user_classes_stop_count[user_class] = self.user_class_occurrences[user_class] - occurrences + elif self.user_class_occurrences[user_class] < occurrences: + user_classes_spawn_count[user_class] = occurrences - self.user_class_occurrences[user_class] + + # call spawn_users before stopping the users since stop_users + # can be blocking because of the stop_timeout + self.spawn_users(user_classes_spawn_count) + self.stop_users(user_classes_stop_count) + + self.environment.events.spawning_complete.fire(user_class_occurrences=self.user_class_occurrences) + def heartbeat(self): while True: try: @@ -758,6 +862,7 @@ def reset_connection(self): logger.error("Temporary failure when resetting connection: %s, will retry later." % (e)) def worker(self): + last_received_spawn_timestamp = 0 while True: try: msg = self.client.recv() @@ -768,17 +873,19 @@ def worker(self): self.worker_state = STATE_SPAWNING self.client.send(Message("spawning", None, self.client_id)) job = msg.data - self.spawn_rate = job["spawn_rate"] - self.target_user_count = job["num_users"] + if job["timestamp"] <= last_received_spawn_timestamp: + logger.info("Discard spawn message with older or equal timestamp than timestamp of previous spawn message") + continue self.environment.host = job["host"] self.environment.stop_timeout = job["stop_timeout"] if self.spawning_greenlet: # kill existing spawning greenlet before we launch new one self.spawning_greenlet.kill(block=True) self.spawning_greenlet = self.greenlet.spawn( - lambda: self.start(user_count=job["num_users"], spawn_rate=job["spawn_rate"]) + lambda: self.start_worker(job["user_class_occurrences"]) ) self.spawning_greenlet.link_exception(greenlet_exception_handler) + last_received_spawn_timestamp = job["timestamp"] elif msg.type == "stop": self.stop() self.client.send(Message("client_stopped", None, self.client_id)) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 25aeab6492..86016fa85d 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1,33 +1,46 @@ -import mock +import time import unittest +from collections import defaultdict import gevent +import mock from gevent import sleep from gevent.queue import Queue -import greenlet import locust -from locust import runners, constant, LoadTestShape -from locust.main import create_environment -from locust.user import User, TaskSet, task +from locust import ( + LoadTestShape, + between, + constant, + runners, +) from locust.env import Environment -from locust.exception import RPCError, StopUser +from locust.exception import ( + RPCError, + StopUser, +) +from locust.main import create_environment from locust.rpc import Message - from locust.runners import ( LocalRunner, - WorkerNode, - WorkerRunner, STATE_INIT, + STATE_MISSING, + STATE_RUNNING, STATE_SPAWNING, STATE_RUNNING, STATE_MISSING, STATE_STOPPING, STATE_STOPPED, + WorkerNode, + WorkerRunner, ) from locust.stats import RequestStats -from locust.test.testcases import LocustTestCase - +from .testcases import LocustTestCase +from locust.user import ( + TaskSet, + User, + task, +) NETWORK_BROKEN = "network broken" @@ -96,25 +109,6 @@ class HeyAnException(Exception): class TestLocustRunner(LocustTestCase): - def assert_locust_class_distribution(self, expected_distribution, classes): - # Construct a {UserClass => count} dict from a list of user classes - distribution = {} - for user_class in classes: - if not user_class in distribution: - distribution[user_class] = 0 - distribution[user_class] += 1 - expected_str = str({k.__name__: v for k, v in expected_distribution.items()}) - actual_str = str({k.__name__: v for k, v in distribution.items()}) - self.assertEqual( - expected_distribution, - distribution, - "Expected a User class distribution of %s but found %s" - % ( - expected_str, - actual_str, - ), - ) - def test_cpu_warning(self): _monitor_interval = runners.CPU_MONITOR_INTERVAL runners.CPU_MONITOR_INTERVAL = 2.0 @@ -131,48 +125,13 @@ def cpu_task(self): environment = Environment(user_classes=[CpuUser]) runner = LocalRunner(environment) self.assertFalse(runner.cpu_warning_emitted) - runner.spawn_users(1, 1, wait=False) + runner.spawn_users({CpuUser.__name__: 1}, wait=False) sleep(2.5) runner.quit() self.assertTrue(runner.cpu_warning_emitted) finally: runners.CPU_MONITOR_INTERVAL = _monitor_interval - def test_weight_locusts(self): - class BaseUser(User): - pass - - class L1(BaseUser): - weight = 101 - - class L2(BaseUser): - weight = 99 - - class L3(BaseUser): - weight = 100 - - runner = Environment(user_classes=[L1, L2, L3]).create_local_runner() - self.assert_locust_class_distribution({L1: 10, L2: 9, L3: 10}, runner.weight_users(29)) - self.assert_locust_class_distribution({L1: 10, L2: 10, L3: 10}, runner.weight_users(30)) - self.assert_locust_class_distribution({L1: 11, L2: 10, L3: 10}, runner.weight_users(31)) - - def test_weight_locusts_fewer_amount_than_user_classes(self): - class BaseUser(User): - pass - - class L1(BaseUser): - weight = 101 - - class L2(BaseUser): - weight = 99 - - class L3(BaseUser): - weight = 100 - - runner = Environment(user_classes=[L1, L2, L3]).create_local_runner() - self.assertEqual(1, len(runner.weight_users(1))) - self.assert_locust_class_distribution({L1: 1}, runner.weight_users(1)) - def test_kill_locusts(self): triggered = [False] @@ -186,11 +145,11 @@ def trigger(self): triggered[0] = True runner = Environment(user_classes=[BaseUser]).create_local_runner() - runner.spawn_users(2, spawn_rate=2, wait=False) + runner.spawn_users({BaseUser.__name__: 2}, wait=False) self.assertEqual(2, len(runner.user_greenlets)) g1 = list(runner.user_greenlets)[0] g2 = list(runner.user_greenlets)[1] - runner.stop_users(2) + runner.stop_users({BaseUser.__name__: 2}) self.assertEqual(0, len(runner.user_greenlets)) self.assertTrue(g1.dead) self.assertTrue(g2.dead) @@ -198,7 +157,7 @@ def trigger(self): def test_start_event(self): class MyUser(User): - wait_time = constant(1) + wait_time = constant(2) task_run_count = 0 @task @@ -317,14 +276,15 @@ def my_task(self): response_time=666, response_length=1337, ) - sleep(2) + # Make sure each user only run this task once during the test + sleep(30) environment = Environment(user_classes=[MyUser], reset_stats=True) runner = LocalRunner(environment) - runner.start(user_count=6, spawn_rate=12, wait=False) - sleep(0.25) + runner.start(user_count=6, spawn_rate=1, wait=False) + sleep(3) self.assertGreaterEqual(runner.stats.get("/test", "GET").num_requests, 3) - sleep(0.3) + sleep(3.25) self.assertLessEqual(runner.stats.get("/test", "GET").num_requests, 1) runner.quit() @@ -369,7 +329,7 @@ def on_stop(self): BaseUser.stop_triggered = True runner = Environment(user_classes=[BaseUser]).create_local_runner() - runner.spawn_users(1, 1, wait=False) + runner.spawn_users({BaseUser.__name__: 1}, wait=False) timeout = gevent.Timeout(0.5) timeout.start() try: @@ -394,7 +354,7 @@ def on_stop(self): BaseUser.stop_count += 1 runner = Environment(user_classes=[BaseUser]).create_local_runner() - runner.spawn_users(10, 10, wait=False) + runner.spawn_users({BaseUser.__name__: 10}, wait=False) timeout = gevent.Timeout(0.3) timeout.start() try: @@ -407,6 +367,11 @@ def on_stop(self): self.assertEqual(10, BaseUser.stop_count) # verify that all users executed on_stop def test_stop_users_with_spawn_rate(self): + """ + The spawn rate does not have an effect on the rate at which the users are stopped. + It is expected that the excess users will be stopped as soon as possible in parallel + (while respecting the stop_timeout). + """ class MyUser(User): wait_time = constant(1) @@ -418,21 +383,19 @@ def my_task(self): runner = LocalRunner(environment) # Start load test, wait for users to start, then trigger ramp down + ts = time.time() runner.start(10, 10, wait=False) - sleep(1) - runner.start(2, 4, wait=False) - - # Wait a moment and then ensure the user count has started to drop but - # not immediately to user_count - sleep(1) - user_count = len(runner.user_greenlets) - self.assertTrue(user_count > 5, "User count has decreased too quickly: %i" % user_count) - self.assertTrue(user_count < 10, "User count has not decreased at all: %i" % user_count) + runner.spawning_greenlet.join() + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta) + self.assertTrue(runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) - # Wait and ensure load test users eventually dropped to desired count - sleep(2) - user_count = len(runner.user_greenlets) - self.assertTrue(user_count == 2, "User count has not decreased correctly to 2, it is : %i" % user_count) + ts = time.time() + runner.start(2, 4, wait=False) + runner.spawning_greenlet.join() + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, "Expected user count to decrease to 2 instantaneously, instead it took %f" % delta) + self.assertTrue(runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) class TestMasterWorkerRunners(LocustTestCase): @@ -558,11 +521,11 @@ class TestShape(LoadTestShape): def tick(self): run_time = self.get_run_time() if run_time < 2: - return (9, 9) + return 9, 9 elif run_time < 4: - return (21, 21) + return 21, 21 elif run_time < 6: - return (3, 21) + return 3, 21 else: return None @@ -583,7 +546,7 @@ def tick(self): master.start_shape() sleep(1) - # Ensure workers have connected and started the correct amounf of users + # Ensure workers have connected and started the correct amount of users for worker in workers: self.assertEqual(3, worker.user_count, "Shape test has not reached stage 1") # Ensure new stage with more users has been reached @@ -613,7 +576,7 @@ class TestShape(LoadTestShape): def tick(self): run_time = self.get_run_time() if run_time < 10: - return (4, 4) + return 4, 4 else: return None @@ -631,7 +594,7 @@ def tick(self): # Give workers time to connect sleep(0.1) - # Start a shape test and ensure workers have connected and started the correct amounf of users + # Start a shape test and ensure workers have connected and started the correct amount of users master.start_shape() sleep(1) for worker in workers: @@ -643,7 +606,7 @@ def tick(self): for worker in workers: self.assertEqual(0, worker.user_count, "Shape test has not stopped") - # Then restart the test again and ensure workers have connected and started the correct amounf of users + # Then restart the test again and ensure workers have connected and started the correct amount of users master.start_shape() sleep(1) for worker in workers: @@ -659,7 +622,9 @@ def setUp(self): def tearDown(self): super().tearDown() - def get_runner(self): + def get_runner(self, user_classes=None): + if user_classes is not None: + self.environment.user_classes = user_classes return self.environment.create_master_runner("*", 5557) def test_worker_connect(self): @@ -946,9 +911,15 @@ def test_master_current_response_times(self): self.assertEqual(30, master.stats.total.get_current_response_time_percentile(0.5)) self.assertEqual(3000, master.stats.total.get_current_response_time_percentile(0.95)) + @mock.patch("locust.runners.HEARTBEAT_INTERVAL", new=600) def test_rebalance_locust_users_on_worker_connect(self): + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) server.mocked_send(Message("client_ready", None, "zeh_fake_client1")) self.assertEqual(1, len(master.clients)) self.assertTrue( @@ -956,26 +927,34 @@ def test_rebalance_locust_users_on_worker_connect(self): ) master.start(100, 20) - self.assertEqual(1, len(server.outbox)) - client_id, msg = server.outbox.pop() - self.assertEqual(100, msg.data["num_users"]) - self.assertEqual(20, msg.data["spawn_rate"]) + self.assertEqual(5, len(server.outbox)) + for i, (_, msg) in enumerate(server.outbox.copy()): + self.assertDictEqual({"TestUser": int((i + 1) * 20)}, msg.data["user_class_occurrences"]) + server.outbox.pop() + + # Normally, this attribute would be updated when the + # master receives the report from the worker. + master.clients["zeh_fake_client1"].user_class_occurrences = {"TestUser": 100} # let another worker connect server.mocked_send(Message("client_ready", None, "zeh_fake_client2")) self.assertEqual(2, len(master.clients)) + sleep(0.1) # give time for messages to be sent to clients self.assertEqual(2, len(server.outbox)) client_id, msg = server.outbox.pop() - self.assertEqual(50, msg.data["num_users"]) - self.assertEqual(10, msg.data["spawn_rate"]) + self.assertEqual({"TestUser": 50}, msg.data["user_class_occurrences"]) client_id, msg = server.outbox.pop() - self.assertEqual(50, msg.data["num_users"]) - self.assertEqual(10, msg.data["spawn_rate"]) + self.assertEqual({"TestUser": 50}, msg.data["user_class_occurrences"]) def test_sends_spawn_data_to_ready_running_spawning_workers(self): """Sends spawn job to running, ready, or spawning workers""" + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) master.clients[1] = WorkerNode(1) master.clients[2] = WorkerNode(2) master.clients[3] = WorkerNode(3) @@ -990,8 +969,13 @@ def test_start_event(self): """ Tests that test_start event is fired """ + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) run_count = [0] @@ -1021,8 +1005,13 @@ def test_stop_event(self): """ Tests that test_stop event is fired """ + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) run_count = [0] @@ -1050,8 +1039,13 @@ def test_stop_event_quit(self): """ Tests that test_stop event is fired when quit() is called directly """ + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) run_count = [0] @@ -1096,32 +1090,40 @@ def test_spawn_uneven_locusts(self): Tests that we can accurately spawn a certain number of locusts, even if it's not an even number of the connected workers """ + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) + for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) master.start(7, 7) self.assertEqual(5, len(server.outbox)) - num_users = 0 - for _, msg in server.outbox: - num_users += msg.data["num_users"] + num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) self.assertEqual(7, num_users, "Total number of locusts that would have been spawned is not 7") def test_spawn_fewer_locusts_than_workers(self): + class TestUser(User): + @task + def my_task(self): + pass + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[TestUser]) + for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) master.start(2, 2) self.assertEqual(5, len(server.outbox)) - num_users = 0 - for _, msg in server.outbox: - num_users += msg.data["num_users"] + num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) self.assertEqual(2, num_users, "Total number of locusts that would have been spawned is not 2") @@ -1135,17 +1137,16 @@ class TestShape(LoadTestShape): def tick(self): run_time = self.get_run_time() if run_time < 2: - return (1, 1) + return 1, 1 elif run_time < 4: - return (2, 2) + return 2, 2 else: return None - self.environment.user_classes = [MyUser] self.environment.shape_class = TestShape() with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[MyUser]) for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) @@ -1155,20 +1156,14 @@ def tick(self): sleep(0.5) # Wait for shape_worker to update user_count - num_users = 0 - for _, msg in server.outbox: - if msg.data: - num_users += msg.data["num_users"] + num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) self.assertEqual( 1, num_users, "Total number of users in first stage of shape test is not 1: %i" % num_users ) # Wait for shape_worker to update user_count again sleep(2) - num_users = 0 - for _, msg in server.outbox: - if msg.data: - num_users += msg.data["num_users"] + num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) self.assertEqual( 3, num_users, "Total number of users in second stage of shape test is not 3: %i" % num_users ) @@ -1187,17 +1182,16 @@ class TestShape(LoadTestShape): def tick(self): run_time = self.get_run_time() if run_time < 2: - return (5, 5) + return 5, 5 elif run_time < 4: - return (-4, 4) + return 1, 5 else: return None - self.environment.user_classes = [MyUser] self.environment.shape_class = TestShape() with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner() + master = self.get_runner(user_classes=[MyUser]) for i in range(5): server.mocked_send(Message("client_ready", None, "fake_client%i" % i)) @@ -1207,20 +1201,20 @@ def tick(self): sleep(0.5) # Wait for shape_worker to update user_count - num_users = 0 - for _, msg in server.outbox: - if msg.data: - num_users += msg.data["num_users"] + num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) self.assertEqual( 5, num_users, "Total number of users in first stage of shape test is not 5: %i" % num_users ) # Wait for shape_worker to update user_count again sleep(2) - num_users = 0 + msgs = defaultdict(dict) for _, msg in server.outbox: - if msg.data: - num_users += msg.data["num_users"] + if not msg.data: + continue + msgs[msg.node_id][msg.data["timestamp"]] = sum(msg.data["user_class_occurrences"].values()) + # Count users for the last received messages + num_users = sum(v[max(v.keys())] for v in msgs.values()) self.assertEqual( 1, num_users, "Total number of users in second stage of shape test is not 1: %i" % num_users ) @@ -1312,9 +1306,10 @@ def tearDown(self): # events.report_to_master._handlers = self._report_to_master_event_handlers super().tearDown() - def get_runner(self, environment=None, user_classes=[]): + def get_runner(self, environment=None, user_classes=None): if environment is None: environment = self.environment + user_classes = user_classes or [] environment.user_classes = user_classes return WorkerRunner(environment, master_host="localhost", master_port=5557) @@ -1343,15 +1338,14 @@ def on_test_start(_environment, **kw): Message( "spawn", { - "spawn_rate": 1, - "num_users": 1, + "timestamp": 1605538584, + "user_class_occurrences": {"MyTestUser": 1}, "host": "", "stop_timeout": 1, }, "dummy_client_id", ) ) - # print("outbox:", client.outbox) # wait for worker to spawn locusts self.assertIn("spawning", [m.type for m in client.outbox]) worker.spawning_greenlet.join() @@ -1386,8 +1380,8 @@ def the_task(self): Message( "spawn", { - "spawn_rate": 1, - "num_users": 1, + "timestamp": 1605538584, + "user_class_occurrences": {"MyTestUser": 1}, "host": "", "stop_timeout": None, }, @@ -1408,6 +1402,87 @@ def the_task(self): # check that locust user did not get to finish self.assertEqual(1, MyTestUser._test_state) + def test_spawn_message_with_older_timestamp_is_rejected(self): + class MyUser(User): + wait_time = constant(1) + + @task + def my_task(self): + pass + + with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: + environment = Environment() + worker = self.get_runner(environment=environment, user_classes=[MyUser]) + + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538584, + "user_class_occurrences": {"MyUser": 10}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + sleep(0.6) + self.assertEqual(STATE_SPAWNING, worker.state) + worker.spawning_greenlet.join() + self.assertEqual(10, worker.user_count) + + # Send same timestamp as the first message + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538584, + "user_class_occurrences": {"MyUser": 9}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + # Still 10 users + self.assertEqual(10, worker.user_count) + + # Send older timestamp than the first message + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538583, + "user_class_occurrences": {"MyUser": 2}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + # Still 10 users + self.assertEqual(10, worker.user_count) + + # Send newer timestamp than the first message + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538585, + "user_class_occurrences": {"MyUser": 2}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + self.assertEqual(2, worker.user_count) + + worker.quit() + def test_change_user_count_during_spawning(self): class MyUser(User): wait_time = constant(1) @@ -1424,8 +1499,8 @@ def my_task(self): Message( "spawn", { - "spawn_rate": 5, - "num_users": 10, + "timestamp": 1605538584, + "user_class_occurrences": {"MyUser": 10}, "host": "", "stop_timeout": None, }, @@ -1438,8 +1513,8 @@ def my_task(self): Message( "spawn", { - "spawn_rate": 5, - "num_users": 9, + "timestamp": 1605538585, + "user_class_occurrences": {"MyUser": 9}, "host": "", "stop_timeout": None, }, @@ -1641,7 +1716,7 @@ class MyTestUser(User): runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time / 2) - runner.stop_users(1) + runner.stop_users({MyTestUser.__name__: 1}) self.assertEqual("first", MyTaskSet.state) runner.quit() environment.runner = None @@ -1650,7 +1725,7 @@ class MyTestUser(User): runner = environment.create_local_runner() runner.start(1, 1) gevent.sleep(short_time) - runner.stop_users(1) + runner.stop_users({MyTestUser.__name__: 1}) self.assertEqual("second", MyTaskSet.state) runner.quit() environment.runner = None @@ -1662,7 +1737,7 @@ class MyTestUser(User): timeout = gevent.Timeout(short_time * 2) timeout.start() try: - runner.stop_users(1) + runner.stop_users({MyTestUser.__name__: 1}) runner.user_greenlets.join() except gevent.Timeout: self.fail("Got Timeout exception. Some locusts must have kept running after iteration finish") @@ -1680,7 +1755,7 @@ def trigger(self): runner = Environment(user_classes=[BaseUser]).create_local_runner() runner.environment.stop_timeout = 1 - runner.spawn_users(1, 1, wait=False) + runner.spawn_users({BaseUser.__name__: 1}, wait=False) timeout = gevent.Timeout(0.5) timeout.start() try: @@ -1718,6 +1793,11 @@ def on_test_stop_fail(*args, **kwargs): self.assertEqual(2, test_stop_run[0]) def test_stop_timeout_with_ramp_down(self): + """ + The spawn rate does not have an effect on the rate at which the users are stopped. + It is expected that the excess users will be stopped as soon as possible in parallel + (while respecting the stop_timeout). + """ class MyTaskSet(TaskSet): @task def my_task(self): @@ -1730,18 +1810,16 @@ class MyTestUser(User): runner = environment.create_local_runner() # Start load test, wait for users to start, then trigger ramp down + ts = time.time() runner.start(10, 10, wait=False) - sleep(1) - runner.start(2, 4, wait=False) + runner.spawning_greenlet.join() + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta) + self.assertTrue(runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) - # Wait a moment and then ensure the user count has started to drop but - # not immediately to user_count - sleep(1) - user_count = len(runner.user_greenlets) - self.assertTrue(user_count > 5, "User count has decreased too quickly: %i" % user_count) - self.assertTrue(user_count < 10, "User count has not decreased at all: %i" % user_count) - - # Wait and ensure load test users eventually dropped to desired count - sleep(2) - user_count = len(runner.user_greenlets) - self.assertTrue(user_count == 2, "User count has not decreased correctly to 2, it is : %i" % user_count) + ts = time.time() + runner.start(2, 4, wait=False) + runner.spawning_greenlet.join() + delta = time.time() - ts + self.assertTrue(1 <= delta <= 1.01, "Expected user count to decrease to 2 in 1s, instead it took %f" % delta) + self.assertTrue(runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) diff --git a/locust/test/test_stats.py b/locust/test/test_stats.py index b778450642..e6dda31b98 100644 --- a/locust/test/test_stats.py +++ b/locust/test/test_stats.py @@ -445,31 +445,37 @@ def t(self): environment = Environment(user_classes=[TestUser]) stats_writer = StatsCSVFileWriter(environment, PERCENTILES_TO_REPORT, self.STATS_BASE_NAME, full_history=True) runner = environment.create_local_runner() - runner.start(3, 5) # spawn a user every _TEST_CSV_STATS_INTERVAL_SEC second + # spawn a user every _TEST_CSV_STATS_INTERVAL_SEC second + user_count = 15 + spawn_rate = 5 + assert 1 / 5 == _TEST_CSV_STATS_INTERVAL_SEC + runner_greenlet = gevent.spawn(runner.start, user_count, spawn_rate) gevent.sleep(0.1) greenlet = gevent.spawn(stats_writer) - gevent.sleep(0.6) + gevent.sleep(user_count / spawn_rate) gevent.kill(greenlet) stats_writer.close_files() runner.stop() + gevent.kill(runner_greenlet) with open(self.STATS_HISTORY_FILENAME) as f: reader = csv.DictReader(f) rows = [r for r in reader] - self.assertEqual(6, len(rows)) - for i in range(3): - row = rows.pop(0) - self.assertEqual("%i" % (i + 1), row["User Count"]) - self.assertEqual("/", row["Name"]) - self.assertEqual("%i" % (i + 1), row["Total Request Count"]) - self.assertGreaterEqual(int(row["Timestamp"]), start_time) - row = rows.pop(0) - self.assertEqual("%i" % (i + 1), row["User Count"]) - self.assertEqual("Aggregated", row["Name"]) - self.assertEqual("%i" % (i + 1), row["Total Request Count"]) - self.assertGreaterEqual(int(row["Timestamp"]), start_time) + self.assertEqual(2 * user_count, len(rows)) + for i in range(int(user_count / spawn_rate)): + for _ in range(spawn_rate): + row = rows.pop(0) + self.assertEqual("%i" % ((i + 1) * spawn_rate), row["User Count"]) + self.assertEqual("/", row["Name"]) + self.assertEqual("%i" % ((i + 1) * spawn_rate), row["Total Request Count"]) + self.assertGreaterEqual(int(row["Timestamp"]), start_time) + row = rows.pop(0) + self.assertEqual("%i" % ((i + 1) * spawn_rate), row["User Count"]) + self.assertEqual("Aggregated", row["Name"]) + self.assertEqual("%i" % ((i + 1) * spawn_rate), row["Total Request Count"]) + self.assertGreaterEqual(int(row["Timestamp"]), start_time) def test_requests_csv_quote_escaping(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: diff --git a/locust/user/users.py b/locust/user/users.py index 5add2c7099..695910d1d6 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -185,6 +185,14 @@ def stop(self, force=False): self._state = LOCUST_STATE_STOPPING return False + @property + def group(self): + return self._group + + @property + def greenlet(self): + return self._greenlet + class HttpUser(User): """ From c5799df29cd505104f93b22b99fcecc4f4091986 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 15:37:11 -0500 Subject: [PATCH 011/139] Add more tests + code cleanup --- locust/dispatch.py | 30 ----- locust/env.py | 1 - locust/runners.py | 19 +-- locust/test/test_dispatch.py | 133 +++++++++++++++++++++ locust/test/test_env.py | 30 +++++ locust/test/test_runners.py | 222 +++++++++++++++++++++++++++++++++++ 6 files changed, 395 insertions(+), 40 deletions(-) create mode 100644 locust/test/test_env.py diff --git a/locust/dispatch.py b/locust/dispatch.py index 6fcfe019b3..c79120d98d 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -123,7 +123,6 @@ def dispatch_users( yield balanced_users -# TODO: test def number_of_users_left_to_dispatch( dispatched_users: Dict[str, Dict[str, int]], balanced_users: Dict[str, Dict[str, int]], @@ -161,7 +160,6 @@ def distribute_current_user_class_among_workers( return done, number_of_users_in_current_dispatch -# TODO: test def all_users_have_been_dispatched( dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], @@ -174,7 +172,6 @@ def all_users_have_been_dispatched( ) -# TODO: test def all_users_of_current_class_have_been_dispatched( dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], @@ -186,33 +183,6 @@ def all_users_of_current_class_have_been_dispatched( ) -# TODO: test -def add_dispatched_users( - dispatched_users1: Dict[str, Dict[str, int]], - dispatched_users2: Dict[str, Dict[str, int]], -) -> Dict[str, Dict[str, int]]: - worker_node_ids = sorted( - set(dispatched_users1.keys()).union( - dispatched_users2.keys() - ) - ) - user_classes = sorted( - set(y for x in dispatched_users1.values() for y in x.keys()).union( - y for x in dispatched_users2.values() for y in x.keys() - ) - ) - return { - worker_node_id: { - user_class: ( - dispatched_users1.get(worker_node_id, {}).get(user_class, 0) - + dispatched_users2.get(worker_node_id, {}).get(user_class, 0) - ) - for user_class in user_classes - } - for worker_node_id in worker_node_ids - } - - def balance_users_among_workers( worker_nodes, # type: List[WorkerNode] user_class_occurrences: Dict[str, int], diff --git a/locust/env.py b/locust/env.py index 3aaa98da2c..dbef7958d4 100644 --- a/locust/env.py +++ b/locust/env.py @@ -194,7 +194,6 @@ def _filter_tasks_by_tags(self): for user_class in self.user_classes: filter_tasks_by_tags(user_class, self.tags, self.exclude_tags) - # TODO: Test this @property def user_classes_by_name(self) -> Dict[str, Type[User]]: return {u.__name__: u for u in self.user_classes} diff --git a/locust/runners.py b/locust/runners.py index 43bfe4c375..9e9e97b7a4 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -79,7 +79,7 @@ def __init__(self, environment): self.worker_cpu_warning_emitted = False self.greenlet.spawn(self.monitor_cpu).link_exception(greenlet_exception_handler) self.exceptions = {} - self.target_user_count = None + self.target_user_class_occurrences: Dict[str, int] = {} # set up event listeners for recording requests def on_request_success(request_type, name, response_time, response_length, **kwargs): @@ -130,7 +130,6 @@ def user_count(self): """ return len(self.user_greenlets) - # TODO: Test this @property def user_class_occurrences(self) -> Dict[str, int]: """ @@ -262,9 +261,11 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): if wait and user_count - self.user_count > spawn_rate: raise ValueError("wait is True but the amount of users to add is greater than the spawn rate") + self.target_user_class_occurrences = weight_users(self.user_classes, user_count) + users_dispatcher = dispatch_users( worker_nodes=[WorkerNode(id="dummy")], - user_class_occurrences=weight_users(self.user_classes, user_count), + user_class_occurrences=self.target_user_class_occurrences, spawn_rate=spawn_rate, ) @@ -371,6 +372,10 @@ def log_exception(self, node_id, msg, formatted_tb): row["nodes"].add(node_id) self.exceptions[key] = row + @property + def target_user_count(self) -> int: + return sum(self.target_user_class_occurrences.values()) + class LocalRunner(Runner): """ @@ -498,7 +503,6 @@ def __init__(self, environment, master_bind_host, master_bind_port): self.worker_cpu_warning_emitted = False self.master_bind_host = master_bind_host self.master_bind_port = master_bind_port - self.target_user_class_occurrences: Dict[str, int] = {} self.spawn_rate: float = 0 self.clients = WorkerNodes() @@ -744,11 +748,6 @@ def client_listener(self): def worker_count(self): return len(self.clients.ready) + len(self.clients.spawning) + len(self.clients.running) - # TODO: Test this - @property - def target_user_count(self) -> int: - return sum(self.user_class_occurrences.values()) - class WorkerRunner(DistributedRunner): """ @@ -812,6 +811,8 @@ def start_worker(self, user_class_occurrences: Dict[str, int], **kwargs): :param user_class_occurrences: Users to run """ + self.target_user_class_occurrences = user_class_occurrences + if self.worker_state != STATE_RUNNING and self.worker_state != STATE_SPAWNING: self.stats.clear_all() self.exceptions = {} diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 061e276892..1cfb52f75c 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -2,8 +2,11 @@ import unittest from locust.dispatch import ( + all_users_have_been_dispatched, + all_users_of_current_class_have_been_dispatched, balance_users_among_workers, dispatch_users, + number_of_users_left_to_dispatch, ) from locust.runners import WorkerNode @@ -1480,3 +1483,133 @@ def test_dispatch_users_to_3_workers(self): self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) + + +class TestNumberOfUsersLeftToDispatch(unittest.TestCase): + maxDiff = None + + def test_number_of_users_left_to_dispatch(self): + user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} + balanced_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + + dispatched_users = { + "Worker1": {"User1": 5, "User2": 2, "User3": 6}, + "Worker2": {"User1": 5, "User2": 2, "User3": 6}, + } + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + self.assertEqual(0, result) + + dispatched_users = { + "Worker1": {"User1": 2, "User2": 0, "User3": 4}, + "Worker2": {"User1": 2, "User2": 0, "User3": 4}, + } + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + self.assertEqual(4, result) + + dispatched_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 0, "User3": 4}, + } + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + self.assertEqual(1, result) + + dispatched_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + self.assertEqual(0, result) + + +class AllUsersHaveBeenDispatched(unittest.TestCase): + maxDiff = None + + def test_all_users_have_been_dispatched(self): + user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} + effective_balanced_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + + dispatched_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + self.assertTrue(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + + dispatched_users = { + "Worker1": {"User1": 4, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + self.assertTrue(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + + dispatched_users = { + "Worker1": {"User1": 2, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + self.assertFalse(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + + dispatched_users = { + "Worker1": {"User1": 0, "User2": 0, "User3": 0}, + "Worker2": {"User1": 0, "User2": 0, "User3": 0}, + } + self.assertFalse(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + + dispatched_users = { + "Worker1": {"User1": 4, "User2": 0, "User3": 0}, + "Worker2": {"User1": 4, "User2": 0, "User3": 0}, + } + self.assertFalse(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + + +class TestAllUsersOfCurrentClassHaveBeenDispatched(unittest.TestCase): + maxDiff = None + + def test_all_users_of_current_class_have_been_dispatched(self): + effective_balanced_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + + dispatched_users = { + "Worker1": {"User1": 3, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + + dispatched_users = { + "Worker1": {"User1": 4, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + + dispatched_users = { + "Worker1": {"User1": 2, "User2": 1, "User3": 4}, + "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + } + self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + + dispatched_users = { + "Worker1": {"User1": 0, "User2": 0, "User3": 0}, + "Worker2": {"User1": 0, "User2": 0, "User3": 0}, + } + self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + + dispatched_users = { + "Worker1": {"User1": 4, "User2": 0, "User3": 0}, + "Worker2": {"User1": 4, "User2": 0, "User3": 0}, + } + self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) diff --git a/locust/test/test_env.py b/locust/test/test_env.py new file mode 100644 index 0000000000..6fa23d8155 --- /dev/null +++ b/locust/test/test_env.py @@ -0,0 +1,30 @@ +from locust import ( + constant, +) +from locust.env import Environment +from locust.user import ( + User, + task, +) +from .testcases import LocustTestCase + + +class TestEnvironment(LocustTestCase): + def test_user_class_occurrences(self): + class MyUser1(User): + wait_time = constant(0) + + @task + def my_task(self): + pass + + class MyUser2(User): + wait_time = constant(0) + + @task + def my_task(self): + pass + + environment = Environment(user_classes=[MyUser1, MyUser2]) + + self.assertDictEqual({"MyUser1": MyUser1, "MyUser2": MyUser2}, environment.user_classes_by_name) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 86016fa85d..df1f7093b3 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -397,6 +397,62 @@ def my_task(self): self.assertTrue(0 <= delta <= 0.01, "Expected user count to decrease to 2 instantaneously, instead it took %f" % delta) self.assertTrue(runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) + def test_attributes_populated_when_calling_start(self): + class MyUser1(User): + wait_time = constant(0) + + @task + def my_task(self): + pass + + class MyUser2(User): + wait_time = constant(0) + + @task + def my_task(self): + pass + + environment = Environment(user_classes=[MyUser1, MyUser2]) + runner = LocalRunner(environment) + + runner.start(user_count=10, spawn_rate=5, wait=False) + runner.spawning_greenlet.join() + self.assertDictEqual({"MyUser1": 5, "MyUser2": 5}, runner.user_class_occurrences) + + runner.start(user_count=5, spawn_rate=5, wait=False) + runner.spawning_greenlet.join() + self.assertDictEqual({"MyUser1": 3, "MyUser2": 2}, runner.user_class_occurrences) + + runner.quit() + + def test_user_class_occurrences(self): + class MyUser1(User): + wait_time = constant(0) + + @task + def my_task(self): + pass + + class MyUser2(User): + wait_time = constant(0) + + @task + def my_task(self): + pass + + environment = Environment(user_classes=[MyUser1, MyUser2]) + runner = LocalRunner(environment) + + runner.start(user_count=10, spawn_rate=5, wait=False) + runner.spawning_greenlet.join() + self.assertDictEqual({"MyUser1": 5, "MyUser2": 5}, runner.user_class_occurrences) + + runner.start(user_count=5, spawn_rate=5, wait=False) + runner.spawning_greenlet.join() + self.assertDictEqual({"MyUser1": 3, "MyUser2": 2}, runner.user_class_occurrences) + + runner.quit() + class TestMasterWorkerRunners(LocustTestCase): def test_distributed_integration_run(self): @@ -1296,6 +1352,68 @@ def test_master_reset_connection(self): self.assertEqual(1, len(master.clients)) master.quit() + def test_attributes_populated_when_calling_start(self): + class MyUser1(User): + @task + def my_task(self): + pass + + class MyUser2(User): + @task + def my_task(self): + pass + + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: + master = self.get_runner(user_classes=[MyUser1, MyUser2]) + + server.mocked_send(Message("client_ready", None, "fake_client1")) + + master.start(7, 7) + self.assertEqual({"MyUser1": 4, "MyUser2": 3}, master.target_user_class_occurrences) + self.assertEqual(7, master.target_user_count) + self.assertEqual(7, master.spawn_rate) + + master.start(10, 10) + self.assertEqual({"MyUser1": 5, "MyUser2": 5}, master.target_user_class_occurrences) + self.assertEqual(10, master.target_user_count) + self.assertEqual(10, master.spawn_rate) + + master.start(1, 3) + self.assertEqual({"MyUser1": 1, "MyUser2": 0}, master.target_user_class_occurrences) + self.assertEqual(1, master.target_user_count) + self.assertEqual(3, master.spawn_rate) + + def test_user_class_occurrences(self): + class MyUser1(User): + @task + def my_task(self): + pass + + class MyUser2(User): + @task + def my_task(self): + pass + + with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: + master = self.get_runner(user_classes=[MyUser1, MyUser2]) + + server.mocked_send(Message("client_ready", None, "fake_client1")) + + master.start(7, 7) + self.assertEqual({"MyUser1": 4, "MyUser2": 3}, master.target_user_class_occurrences) + self.assertEqual(7, master.target_user_count) + self.assertEqual(7, master.spawn_rate) + + master.start(10, 10) + self.assertEqual({"MyUser1": 5, "MyUser2": 5}, master.target_user_class_occurrences) + self.assertEqual(10, master.target_user_count) + self.assertEqual(10, master.spawn_rate) + + master.start(1, 3) + self.assertEqual({"MyUser1": 1, "MyUser2": 0}, master.target_user_class_occurrences) + self.assertEqual(1, master.target_user_count) + self.assertEqual(3, master.spawn_rate) + class TestWorkerRunner(LocustTestCase): def setUp(self): @@ -1526,6 +1644,110 @@ def my_task(self): self.assertEqual(9, len(worker.user_greenlets)) worker.quit() + def test_attributes_populated_when_calling_start_worker(self): + class MyUser1(User): + wait_time = constant(1) + + @task + def my_task(self): + pass + + class MyUser2(User): + wait_time = constant(1) + + @task + def my_task(self): + pass + + with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: + environment = Environment() + worker = self.get_runner(environment=environment, user_classes=[MyUser1, MyUser2]) + + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538584, + "user_class_occurrences": {"MyUser1": 10, "MyUser2": 10}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 10, "MyUser2": 10}) + + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538585, + "user_class_occurrences": {"MyUser1": 1, "MyUser2": 2}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) + + worker.quit() + + def test_user_class_occurrences(self): + class MyUser1(User): + wait_time = constant(1) + + @task + def my_task(self): + pass + + class MyUser2(User): + wait_time = constant(1) + + @task + def my_task(self): + pass + + with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: + environment = Environment() + worker = self.get_runner(environment=environment, user_classes=[MyUser1, MyUser2]) + + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538584, + "user_class_occurrences": {"MyUser1": 10, "MyUser2": 10}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + self.assertDictEqual(worker.target_user_class_occurrences, {"MyUser1": 10, "MyUser2": 10}) + self.assertEqual(worker.target_user_count, 20) + + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538585, + "user_class_occurrences": {"MyUser1": 1, "MyUser2": 2}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + worker.spawning_greenlet.join() + self.assertDictEqual(worker.target_user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) + self.assertEqual(worker.target_user_count, 3) + + worker.quit() + class TestMessageSerializing(unittest.TestCase): def test_message_serialize(self): From 41f6ac20fc89e30ad73076aac56e93799eab318b Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 17:11:33 -0500 Subject: [PATCH 012/139] Update signature of on_spawning_complete to take user_class_occurrences instead of user_count --- locust/event.py | 2 +- locust/runners.py | 6 +++--- locust/stats.py | 2 +- locust/test/test_runners.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/locust/event.py b/locust/event.py index f79942c764..092e2db403 100644 --- a/locust/event.py +++ b/locust/event.py @@ -119,7 +119,7 @@ class Events: Event arguments: - :param user_count: Number of users that were spawned + :param user_class_occurrences: Number of users for each class that were spawned """ quitting: EventHook diff --git a/locust/runners.py b/locust/runners.py index 9e9e97b7a4..020e978af1 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -94,7 +94,7 @@ def on_request_failure(request_type, name, response_time, response_length, excep self.connection_broken = False # register listener that resets stats when spawning is complete - def on_spawning_complete(user_count): + def on_spawning_complete(user_class_occurrences): self.update_state(STATE_RUNNING) if environment.reset_stats: logger.info("Resetting stats\n") @@ -298,7 +298,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): self.spawn_users(user_classes_spawn_count, wait) self.stop_users(user_classes_stop_count) - self.environment.events.spawning_complete.fire(user_count=self.user_count) + self.environment.events.spawning_complete.fire(user_class_occurrences=self.target_user_class_occurrences) def start_shape(self): if self.shape_greenlet: @@ -727,7 +727,7 @@ def client_listener(self): self.clients[msg.node_id].state = STATE_RUNNING self.clients[msg.node_id].user_class_occurrences = msg.data["user_class_occurrences"] if len(self.clients.spawning) == 0: - self.environment.events.spawning_complete.fire(user_count=self.user_count) + self.environment.events.spawning_complete.fire(user_class_occurrences=self.user_class_occurrences) elif msg.type == "quit": if msg.node_id in self.clients: del self.clients[msg.node_id] diff --git a/locust/stats.py b/locust/stats.py index 6c9c524d5f..31f807d29a 100644 --- a/locust/stats.py +++ b/locust/stats.py @@ -698,7 +698,7 @@ def on_worker_report(client_id, data): for stats_data in data["stats"]: entry = StatsEntry.unserialize(stats_data) request_key = (entry.name, entry.method) - if not request_key in stats.entries: + if request_key not in stats.entries: stats.entries[request_key] = StatsEntry(stats, entry.name, entry.method, use_response_times_cache=True) stats.entries[request_key].extend(entry) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index df1f7093b3..f6124a62e4 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1996,13 +1996,13 @@ def my_task(self): pass test_stop_run = [0] - environment = Environment(user_classes=[User]) + environment = Environment(user_classes=[MyUser]) def on_test_stop_ok(*args, **kwargs): test_stop_run[0] += 1 def on_test_stop_fail(*args, **kwargs): - assert 0 + assert False environment.events.test_stop.add_listener(on_test_stop_ok) environment.events.test_stop.add_listener(on_test_stop_fail) From 3d41b581d056e87107f23db6857a4e45a60a7215 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 18:11:22 -0500 Subject: [PATCH 013/139] Bugfix --- locust/runners.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 020e978af1..cf786bf99a 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -261,6 +261,10 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): if wait and user_count - self.user_count > spawn_rate: raise ValueError("wait is True but the amount of users to add is greater than the spawn rate") + for user_class in self.user_classes: + if self.environment.host is not None: + user_class.host = self.environment.host + self.target_user_class_occurrences = weight_users(self.user_classes, user_count) users_dispatcher = dispatch_users( @@ -556,6 +560,10 @@ def start(self, user_count: int, spawn_rate: float, **kwargs): ) return + for user_class in self.user_classes: + if self.environment.host is not None: + user_class.host = self.environment.host + self.target_user_class_occurrences = weight_users(self.user_classes, user_count) self.spawn_rate = spawn_rate From 5d5f732a9567a29f781d57cf3ef16c3ae2d4b578 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 18:16:35 -0500 Subject: [PATCH 014/139] Bugfix + fix typo --- locust/runners.py | 8 +------- locust/test/mock_locustfile.py | 4 ++-- locust/test/test_main.py | 12 ++++++------ 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index cf786bf99a..2cbc038ce0 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -172,8 +172,6 @@ def spawn(user_class: str, spawn_count: int): n = 0 while n < spawn_count: new_user = self.user_classes_by_name[user_class](self.environment) - if self.environment.host is not None: - new_user.host = self.environment.host new_user.start(self.user_greenlets) n += 1 if n % 10 == 0 or n == spawn_count: @@ -352,11 +350,7 @@ def stop(self): self.shape_greenlet = None self.shape_last_state = None - user_classes_stop_count = { - user_class: self.user_class_occurrences[user_class] - for user_class, occurrences in self.user_class_occurrences.items() - } - self.stop_users(user_classes_stop_count) + self.stop_users(self.user_class_occurrences) self.update_state(STATE_STOPPED) diff --git a/locust/test/mock_locustfile.py b/locust/test/mock_locustfile.py index 0063f3d76d..05cb1e485d 100644 --- a/locust/test/mock_locustfile.py +++ b/locust/test/mock_locustfile.py @@ -5,7 +5,7 @@ from contextlib import contextmanager -MOCK_LOUCSTFILE_CONTENT = ''' +MOCK_LOCUSTFILE_CONTENT = ''' """This is a mock locust file for unit testing""" from locust import HttpUser, TaskSet, task, between, LoadTestShape @@ -40,7 +40,7 @@ class MockedLocustfile: @contextmanager -def mock_locustfile(filename_prefix="mock_locustfile", content=MOCK_LOUCSTFILE_CONTENT): +def mock_locustfile(filename_prefix="mock_locustfile", content=MOCK_LOCUSTFILE_CONTENT): mocked = MockedLocustfile() mocked.directory = os.path.dirname(os.path.abspath(__file__)) mocked.filename = "%s_%s_%i.py" % ( diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 1452e86f69..446dec14b8 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -14,7 +14,7 @@ from locust.argument_parser import parse_options from locust.main import create_environment from locust.user import HttpUser, User, TaskSet -from .mock_locustfile import mock_locustfile, MOCK_LOUCSTFILE_CONTENT +from .mock_locustfile import mock_locustfile, MOCK_LOCUSTFILE_CONTENT from .testcases import LocustTestCase from .util import temporary_file, get_free_tcp_port @@ -71,8 +71,8 @@ def test_return_docstring_and_user_classes(self): def test_with_shape_class(self): content = ( - MOCK_LOUCSTFILE_CONTENT - + """class LoadTestShape(LoadTestShape): + MOCK_LOCUSTFILE_CONTENT + + """class LoadTestShape(LoadTestShape): pass """ ) @@ -236,7 +236,7 @@ def test_default_headless_spawn_options(self): .decode("utf-8") .strip() ) - self.assertIn("Spawning 1 users at the rate 1 users/s", output) + self.assertIn("Spawning additional {\"UserSubclass\": 1} ({\"UserSubclass\": 0} already running)...", output) def test_headless_spawn_options_wo_run_time(self): with mock_locustfile() as mocked: @@ -255,13 +255,13 @@ def test_headless_spawn_options_wo_run_time(self): self.assertIn("Shutting down (exit code 0), bye", stderr) def test_default_headless_spawn_options_with_shape(self): - content = MOCK_LOUCSTFILE_CONTENT + textwrap.dedent( + content = MOCK_LOCUSTFILE_CONTENT + textwrap.dedent( """ class LoadTestShape(LoadTestShape): def tick(self): run_time = self.get_run_time() if run_time < 2: - return (10, 1) + return (10, 1) return None """ From 7450c9f6990d9ad961d3e63d56d78f717ba2ac6d Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 18:36:02 -0500 Subject: [PATCH 015/139] Fix failing test --- locust/test/test_main.py | 4 ++-- locust/test/test_runners.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 446dec14b8..56fc90a92e 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -71,8 +71,8 @@ def test_return_docstring_and_user_classes(self): def test_with_shape_class(self): content = ( - MOCK_LOCUSTFILE_CONTENT - + """class LoadTestShape(LoadTestShape): + MOCK_LOCUSTFILE_CONTENT + + """class LoadTestShape(LoadTestShape): pass """ ) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index f6124a62e4..567ae5efb3 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -5,6 +5,7 @@ import gevent import mock from gevent import sleep +from gevent.pool import Group from gevent.queue import Queue import locust @@ -1605,6 +1606,12 @@ def test_change_user_count_during_spawning(self): class MyUser(User): wait_time = constant(1) + def start(self, group: Group): + # We do this so that the spawning does not finish + # too quickly + gevent.sleep(0.1) + return super().start(group) + @task def my_task(self): pass From bdce0a308fbfd48df846c12cec8768c3edd6acd9 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 18:41:27 -0500 Subject: [PATCH 016/139] Fix failing test --- locust/test/test_runners.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 567ae5efb3..289eabe0a5 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1525,6 +1525,12 @@ def test_spawn_message_with_older_timestamp_is_rejected(self): class MyUser(User): wait_time = constant(1) + def start(self, group: Group): + # We do this so that the spawning does not finish + # too quickly + gevent.sleep(0.1) + return super().start(group) + @task def my_task(self): pass From 5cad9a4ed0f6d04dab8eb09a635e125c494ba8c8 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 19:37:28 -0500 Subject: [PATCH 017/139] Update input event to work with new users distribution logic --- locust/input_events.py | 29 +++++++++++++++++++++++++---- locust/main.py | 19 ++++++------------- locust/test/test_main.py | 26 ++++++++++++++++++-------- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/locust/input_events.py b/locust/input_events.py index f10951e68c..619fad936c 100644 --- a/locust/input_events.py +++ b/locust/input_events.py @@ -1,8 +1,18 @@ +from typing import ( + Dict, + Tuple, +) + import gevent import logging import os import sys +from locust.runners import ( + Runner, + STATE_SPAWNING, +) + if os.name == "nt": from win32api import STD_INPUT_HANDLE from win32console import ( @@ -86,7 +96,7 @@ def get_poller(): return UnixKeyPoller() -def input_listener(key_to_func_map): +def input_listener(key_to_user_params: Dict[str, Tuple[int, float]], runner: Runner): def input_listener_func(): try: poller = get_poller() @@ -94,13 +104,24 @@ def input_listener_func(): logging.info(e) return + user_count = 0 try: while True: input = poller.poll() if input: - for key in key_to_func_map: - if input == key: - key_to_func_map[key]() + try: + user_params = key_to_user_params[input] + except KeyError: + continue + user_delta, spawn_rate = user_params + if runner.state == STATE_SPAWNING and user_delta > 0: + logging.warning("Already spawning users, can't spawn more right now") + continue + elif runner.state == STATE_SPAWNING and user_delta < 0: + logging.warning("Spawning users, can't stop right now") + continue + user_count = max(0, user_count + user_delta) + runner.start(user_count, spawn_rate) else: gevent.sleep(0.2) except Exception as e: diff --git a/locust/main.py b/locust/main.py index 5add22205a..e2a2b08ecf 100644 --- a/locust/main.py +++ b/locust/main.py @@ -351,19 +351,12 @@ def timelimit_stop(): input_listener_greenlet = gevent.spawn( input_listener( { - "w": lambda: runner.spawn_users(1, 100) - if runner.state != "spawning" - else logging.warning("Already spawning users, can't spawn more right now"), - "W": lambda: runner.spawn_users(10, 100) - if runner.state != "spawning" - else logging.warning("Already spawning users, can't spawn more right now"), - "s": lambda: runner.stop_users(1) - if runner.state != "spawning" - else logging.warning("Spawning users, can't stop right now"), - "S": lambda: runner.stop_users(10) - if runner.state != "spawning" - else logging.warning("Spawning users, can't stop right now"), - } + "w": (1, 100), + "W": (10, 100), + "s": (-1, 100), + "S": (-10, 100), + }, + runner, ) ) input_listener_greenlet.link_exception(greenlet_exception_handler) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 56fc90a92e..08ee91d2ab 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -338,7 +338,7 @@ def t(self): mocked.file_path, "--headless", "--run-time", - "4s", + "7s", "-u", "0", ] @@ -351,22 +351,32 @@ def t(self): gevent.sleep(1) stdin.write(b"w") - gevent.sleep(0.1) + gevent.sleep(1) stdin.write(b"W") - gevent.sleep(0.1) + gevent.sleep(1) stdin.write(b"s") - gevent.sleep(0.1) + gevent.sleep(1) stdin.write(b"S") + gevent.sleep(1) + # These two should not do anything since we are already at zero users + stdin.write(b"S") gevent.sleep(1) output = proc.communicate()[0].decode("utf-8") stdin.close() - self.assertIn("Spawning 1 users at the rate 100 users/s", output) - self.assertIn("Spawning 10 users at the rate 100 users/s", output) - self.assertIn("1 Users have been stopped", output) - self.assertIn("10 Users have been stopped", output) + self.assertIn("Spawning additional {\"UserSubclass\": 1} ({\"UserSubclass\": 0} already running)...", output) + self.assertIn("0 Users have been stopped, 1 still running", output) + self.assertIn("Spawning additional {\"UserSubclass\": 10} ({\"UserSubclass\": 1} already running)...", output) + self.assertIn("Spawning additional {} ({\"UserSubclass\": 11} already running)...", output) + self.assertIn("1 Users have been stopped, 10 still running", output) + self.assertIn("Spawning additional {} ({\"UserSubclass\": 10} already running)...", output) + self.assertIn("10 Users have been stopped, 0 still running", output) + self.assertIn("Spawning additional {} ({\"UserSubclass\": 0} already running)...", output) + self.assertIn("10 Users have been stopped, 0 still running", output) self.assertIn("Test task is running", output) + self.assertIn("Shutting down (exit code 0), bye.", output) + self.assertEqual(0, proc.returncode) def test_html_report_option(self): with mock_locustfile() as mocked: From dc18e77aa4e02e73bc1d55dddd37da61b4dde47c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 16 Nov 2020 19:44:34 -0500 Subject: [PATCH 018/139] Run Black --- locust/dispatch.py | 60 +- locust/distribution.py | 44 +- locust/env.py | 5 +- locust/runners.py | 28 +- locust/test/test_dispatch.py | 1343 +++++++++++++++++++----------- locust/test/test_distribution.py | 20 +- locust/test/test_main.py | 12 +- locust/test/test_runners.py | 35 +- 8 files changed, 963 insertions(+), 584 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index c79120d98d..d3de20a858 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -16,9 +16,9 @@ def dispatch_users( - worker_nodes, # type: List[WorkerNode] - user_class_occurrences: Dict[str, int], - spawn_rate: float, + worker_nodes, # type: List[WorkerNode] + user_class_occurrences: Dict[str, int], + spawn_rate: float, ) -> Generator[Dict[str, Dict[str, int]], None, None]: initial_dispatched_users = { worker_node.id: { @@ -86,8 +86,9 @@ def dispatch_users( gevent.sleep(max(0.0, wait_between_dispatch - delta)) elif ( - not less_users_than_desired - and number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) <= number_of_users_per_dispatch + not less_users_than_desired + and number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + <= number_of_users_per_dispatch ): yield balanced_users @@ -95,7 +96,9 @@ def dispatch_users( while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): number_of_users_in_current_dispatch = 0 for user_class in user_class_occurrences.keys(): - if all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, user_class): + if all_users_of_current_class_have_been_dispatched( + dispatched_users, effective_balanced_users, user_class + ): continue done, number_of_users_in_current_dispatch = distribute_current_user_class_among_workers( dispatched_users, @@ -124,26 +127,25 @@ def dispatch_users( def number_of_users_left_to_dispatch( - dispatched_users: Dict[str, Dict[str, int]], - balanced_users: Dict[str, Dict[str, int]], - user_class_occurrences: Dict[str, int], + dispatched_users: Dict[str, Dict[str, int]], + balanced_users: Dict[str, Dict[str, int]], + user_class_occurrences: Dict[str, int], ) -> int: return sum( max( 0, - sum(x[user_class] for x in balanced_users.values()) - - sum(x[user_class] for x in dispatched_users.values()) + sum(x[user_class] for x in balanced_users.values()) - sum(x[user_class] for x in dispatched_users.values()), ) for user_class in user_class_occurrences.keys() ) def distribute_current_user_class_among_workers( - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], - user_class: str, - number_of_users_in_current_dispatch: int, - number_of_users_per_dispatch: int, + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + user_class: str, + number_of_users_in_current_dispatch: int, + number_of_users_per_dispatch: int, ): done = False for worker_node_id in itertools.cycle(effective_balanced_users.keys()): @@ -161,9 +163,9 @@ def distribute_current_user_class_among_workers( def all_users_have_been_dispatched( - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], - user_class_occurrences: Dict[str, int], + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + user_class_occurrences: Dict[str, int], ) -> bool: return all( sum(x[user_class] for x in dispatched_users.values()) @@ -173,24 +175,22 @@ def all_users_have_been_dispatched( def all_users_of_current_class_have_been_dispatched( - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], - user_class: str, + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + user_class: str, ) -> bool: - return ( - sum(x[user_class] for x in dispatched_users.values()) - >= sum(x[user_class] for x in effective_balanced_users.values()) + return sum(x[user_class] for x in dispatched_users.values()) >= sum( + x[user_class] for x in effective_balanced_users.values() ) def balance_users_among_workers( - worker_nodes, # type: List[WorkerNode] - user_class_occurrences: Dict[str, int], + worker_nodes, # type: List[WorkerNode] + user_class_occurrences: Dict[str, int], ) -> Dict[str, Dict[str, int]]: balanced_users = { - worker_node.id: { - user_class: 0 for user_class in sorted(user_class_occurrences.keys()) - } for worker_node in worker_nodes + worker_node.id: {user_class: 0 for user_class in sorted(user_class_occurrences.keys())} + for worker_node in worker_nodes } user_class_occurrences = user_class_occurrences.copy() diff --git a/locust/distribution.py b/locust/distribution.py index c74065bef2..3acdfae239 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -10,8 +10,8 @@ def weight_users( - user_classes: List[Type[User]], - number_of_users: int, + user_classes: List[Type[User]], + number_of_users: int, ) -> Dict[str, int]: """ Compute users to spawn @@ -30,14 +30,16 @@ def weight_users( user_class_occurrences = {user_class.__name__: 0 for user_class in user_classes} if number_of_users <= len(user_classes): - user_class_occurrences.update({ - user_class.__name__: 1 - for user_class in sorted( - user_classes, - key=lambda user_class: user_class.weight, - reverse=True, - )[:number_of_users] - }) + user_class_occurrences.update( + { + user_class.__name__: 1 + for user_class in sorted( + user_classes, + key=lambda user_class: user_class.weight, + reverse=True, + )[:number_of_users] + } + ) return user_class_occurrences weights = list(map(attrgetter("weight"), user_classes)) @@ -71,10 +73,10 @@ def weight_users( def _recursive_add_users( - user_classes: List[Type[User]], - number_of_users: int, - user_class_occurrences_candidate: Dict[str, int], - user_class_occurrences_candidates: Dict[float, Dict[str, int]], + user_classes: List[Type[User]], + number_of_users: int, + user_class_occurrences_candidate: Dict[str, int], + user_class_occurrences_candidates: Dict[float, Dict[str, int]], ): if sum(user_class_occurrences_candidate.values()) == number_of_users: distance = distance_from_desired_distribution( @@ -99,10 +101,10 @@ def _recursive_add_users( def _recursive_remove_users( - user_classes: List[Type[User]], - number_of_users: int, - user_class_occurrences_candidate: Dict[str, int], - user_class_occurrences_candidates: Dict[float, Dict[str, int]], + user_classes: List[Type[User]], + number_of_users: int, + user_class_occurrences_candidate: Dict[str, int], + user_class_occurrences_candidates: Dict[float, Dict[str, int]], ): if sum(user_class_occurrences_candidate.values()) == number_of_users: distance = distance_from_desired_distribution( @@ -129,8 +131,8 @@ def _recursive_remove_users( def distance_from_desired_distribution( - user_classes: List[Type[User]], - user_class_occurrences: Dict[str, int], + user_classes: List[Type[User]], + user_class_occurrences: Dict[str, int], ) -> float: user_class_2_actual_percentage = { user_class: 100 * occurrences / sum(user_class_occurrences.values()) @@ -147,4 +149,4 @@ def distance_from_desired_distribution( for user_class, expected_percentage in user_class_2_expected_percentage.items() ] - return math.sqrt(math.fsum(map(lambda x: x**2, differences))) + return math.sqrt(math.fsum(map(lambda x: x ** 2, differences))) diff --git a/locust/env.py b/locust/env.py index dbef7958d4..4cf165d61f 100644 --- a/locust/env.py +++ b/locust/env.py @@ -101,7 +101,10 @@ def __init__( self._filter_tasks_by_tags() def _create_runner( - self, runner_class: Union[Type[LocalRunner], Type[MasterRunner], Type[WorkerRunner]], *args, **kwargs, + self, + runner_class: Union[Type[LocalRunner], Type[MasterRunner], Type[WorkerRunner]], + *args, + **kwargs, ) -> Union[LocalRunner, MasterRunner, WorkerRunner]: if self.runner is not None: raise RunnerAlreadyExistsError("Environment.runner already exists (%s)" % self.runner) diff --git a/locust/runners.py b/locust/runners.py index 2cbc038ce0..7c02998f31 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -227,7 +227,9 @@ def stop_users(self, user_classes_stop_count: Dict[str, int]): ) stop_group.kill(block=True) - logger.info("%i Users have been stopped, %g still running" % (sum(user_classes_stop_count.values()), self.user_count)) + logger.info( + "%i Users have been stopped, %g still running" % (sum(user_classes_stop_count.values()), self.user_count) + ) def monitor_cpu(self): process = psutil.Process() @@ -593,11 +595,13 @@ def start(self, user_count: int, spawn_rate: float, **kwargs): "host": self.environment.host, "stop_timeout": self.environment.stop_timeout, } - dispatch_greenlets.add(gevent.spawn_later( - 0, - self.server.send_to_client, - Message("spawn", data, worker_node_id), - )) + dispatch_greenlets.add( + gevent.spawn_later( + 0, + self.server.send_to_client, + Message("spawn", data, worker_node_id), + ) + ) logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) dispatch_greenlets.join() @@ -780,7 +784,9 @@ def __init__(self, environment, master_host, master_port): # register listener for when all users have spawned, and report it to the master node def on_spawning_complete(user_class_occurrences): - self.client.send(Message("spawning_complete", {"user_class_occurrences": user_class_occurrences}, self.client_id)) + self.client.send( + Message("spawning_complete", {"user_class_occurrences": user_class_occurrences}, self.client_id) + ) self.worker_state = STATE_RUNNING self.environment.events.spawning_complete.add_listener(on_spawning_complete) @@ -877,16 +883,16 @@ def worker(self): self.client.send(Message("spawning", None, self.client_id)) job = msg.data if job["timestamp"] <= last_received_spawn_timestamp: - logger.info("Discard spawn message with older or equal timestamp than timestamp of previous spawn message") + logger.info( + "Discard spawn message with older or equal timestamp than timestamp of previous spawn message" + ) continue self.environment.host = job["host"] self.environment.stop_timeout = job["stop_timeout"] if self.spawning_greenlet: # kill existing spawning greenlet before we launch new one self.spawning_greenlet.kill(block=True) - self.spawning_greenlet = self.greenlet.spawn( - lambda: self.start_worker(job["user_class_occurrences"]) - ) + self.spawning_greenlet = self.greenlet.spawn(lambda: self.start_worker(job["user_class_occurrences"])) self.spawning_greenlet.link_exception(greenlet_exception_handler) last_received_spawn_timestamp = job["timestamp"] elif msg.type == "stop": diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 1cfb52f75c..15dd94c3a3 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -103,7 +103,7 @@ def test_balance_users_among_3_workers(self): expected_balanced_users = { "1": {"User1": 0, "User2": 0, "User3": 0}, "2": {"User1": 0, "User2": 0, "User3": 0}, - '3': {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, } self.assertDictEqual(balanced_users, expected_balanced_users) @@ -125,83 +125,110 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): sleep_time = 1 / 0.15 ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) @@ -222,83 +249,110 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) @@ -319,83 +373,110 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -416,47 +497,62 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -479,47 +575,62 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): sleep_time = 2 / 2.4 ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) @@ -540,29 +651,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -583,29 +703,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -626,11 +755,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -660,65 +792,86 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): sleep_time = 1 / 0.15 ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) @@ -742,65 +895,86 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) @@ -824,65 +998,86 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -906,38 +1101,50 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -963,38 +1170,50 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): sleep_time = 2 / 2.4 ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) @@ -1018,29 +1237,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -1064,20 +1292,26 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -1101,11 +1335,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -1135,29 +1372,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): sleep_time = 1 / 0.15 ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) @@ -1181,29 +1427,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(1.99 <= delta <= 2.01, delta) @@ -1227,29 +1482,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -1273,20 +1537,26 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0.99 <= delta <= 1.01, delta) @@ -1312,20 +1582,26 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): sleep_time = 2 / 2.4 ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) @@ -1349,11 +1625,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -1377,11 +1656,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -1405,11 +1687,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -1438,11 +1723,14 @@ def test_dispatch_users_to_3_workers(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -1471,11 +1759,14 @@ def test_dispatch_users_to_3_workers(self): ) ts = time.time() - self.assertDictEqual(next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }) + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.01, delta) @@ -1538,31 +1829,41 @@ def test_all_users_have_been_dispatched(self): "Worker1": {"User1": 3, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - self.assertTrue(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + self.assertTrue( + all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) + ) dispatched_users = { "Worker1": {"User1": 4, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - self.assertTrue(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + self.assertTrue( + all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) + ) dispatched_users = { "Worker1": {"User1": 2, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - self.assertFalse(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + self.assertFalse( + all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) + ) dispatched_users = { "Worker1": {"User1": 0, "User2": 0, "User3": 0}, "Worker2": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertFalse(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + self.assertFalse( + all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) + ) dispatched_users = { "Worker1": {"User1": 4, "User2": 0, "User3": 0}, "Worker2": {"User1": 4, "User2": 0, "User3": 0}, } - self.assertFalse(all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences)) + self.assertFalse( + all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) + ) class TestAllUsersOfCurrentClassHaveBeenDispatched(unittest.TestCase): @@ -1578,38 +1879,68 @@ def test_all_users_of_current_class_have_been_dispatched(self): "Worker1": {"User1": 3, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") + ) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") + ) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") + ) dispatched_users = { "Worker1": {"User1": 4, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") + ) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") + ) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") + ) dispatched_users = { "Worker1": {"User1": 2, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + self.assertFalse( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") + ) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") + ) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") + ) dispatched_users = { "Worker1": {"User1": 0, "User2": 0, "User3": 0}, "Worker2": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + self.assertFalse( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") + ) + self.assertFalse( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") + ) + self.assertFalse( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") + ) dispatched_users = { "Worker1": {"User1": 4, "User2": 0, "User3": 0}, "Worker2": {"User1": 4, "User2": 0, "User3": 0}, } - self.assertTrue(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3")) + self.assertTrue( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") + ) + self.assertFalse( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") + ) + self.assertFalse( + all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") + ) diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 73f9c500d4..7614cadac5 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -222,7 +222,7 @@ class User3(User): user_classes=[User1, User2, User3], number_of_users=10, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, 'User2': 4, "User3": 4}) + self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 4, "User3": 4}) user_class_occurrences = weight_users( user_classes=[User1, User2, User3], @@ -278,7 +278,23 @@ class User15(User): number_of_users = 1044523783783 user_class_occurrences = weight_users( - user_classes=[User1, User2, User3, User4, User5, User6, User7, User8, User9, User10, User11, User12, User13, User14, User15], + user_classes=[ + User1, + User2, + User3, + User4, + User5, + User6, + User7, + User8, + User9, + User10, + User11, + User12, + User13, + User14, + User15, + ], number_of_users=number_of_users, ) self.assertEqual(sum(user_class_occurrences.values()), number_of_users) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 08ee91d2ab..3b77202224 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -236,7 +236,7 @@ def test_default_headless_spawn_options(self): .decode("utf-8") .strip() ) - self.assertIn("Spawning additional {\"UserSubclass\": 1} ({\"UserSubclass\": 0} already running)...", output) + self.assertIn('Spawning additional {"UserSubclass": 1} ({"UserSubclass": 0} already running)...', output) def test_headless_spawn_options_wo_run_time(self): with mock_locustfile() as mocked: @@ -365,14 +365,14 @@ def t(self): output = proc.communicate()[0].decode("utf-8") stdin.close() - self.assertIn("Spawning additional {\"UserSubclass\": 1} ({\"UserSubclass\": 0} already running)...", output) + self.assertIn('Spawning additional {"UserSubclass": 1} ({"UserSubclass": 0} already running)...', output) self.assertIn("0 Users have been stopped, 1 still running", output) - self.assertIn("Spawning additional {\"UserSubclass\": 10} ({\"UserSubclass\": 1} already running)...", output) - self.assertIn("Spawning additional {} ({\"UserSubclass\": 11} already running)...", output) + self.assertIn('Spawning additional {"UserSubclass": 10} ({"UserSubclass": 1} already running)...', output) + self.assertIn('Spawning additional {} ({"UserSubclass": 11} already running)...', output) self.assertIn("1 Users have been stopped, 10 still running", output) - self.assertIn("Spawning additional {} ({\"UserSubclass\": 10} already running)...", output) + self.assertIn('Spawning additional {} ({"UserSubclass": 10} already running)...', output) self.assertIn("10 Users have been stopped, 0 still running", output) - self.assertIn("Spawning additional {} ({\"UserSubclass\": 0} already running)...", output) + self.assertIn('Spawning additional {} ({"UserSubclass": 0} already running)...', output) self.assertIn("10 Users have been stopped, 0 still running", output) self.assertIn("Test task is running", output) self.assertIn("Shutting down (exit code 0), bye.", output) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 289eabe0a5..8521929987 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -373,6 +373,7 @@ def test_stop_users_with_spawn_rate(self): It is expected that the excess users will be stopped as soon as possible in parallel (while respecting the stop_timeout). """ + class MyUser(User): wait_time = constant(1) @@ -388,15 +389,23 @@ def my_task(self): runner.start(10, 10, wait=False) runner.spawning_greenlet.join() delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta) - self.assertTrue(runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) + self.assertTrue( + 0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta + ) + self.assertTrue( + runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count + ) ts = time.time() runner.start(2, 4, wait=False) runner.spawning_greenlet.join() delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, "Expected user count to decrease to 2 instantaneously, instead it took %f" % delta) - self.assertTrue(runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) + self.assertTrue( + 0 <= delta <= 0.01, "Expected user count to decrease to 2 instantaneously, instead it took %f" % delta + ) + self.assertTrue( + runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count + ) def test_attributes_populated_when_calling_start(self): class MyUser1(User): @@ -1005,6 +1014,7 @@ def my_task(self): def test_sends_spawn_data_to_ready_running_spawning_workers(self): """Sends spawn job to running, ready, or spawning workers""" + class TestUser(User): @task def my_task(self): @@ -1026,6 +1036,7 @@ def test_start_event(self): """ Tests that test_start event is fired """ + class TestUser(User): @task def my_task(self): @@ -1062,6 +1073,7 @@ def test_stop_event(self): """ Tests that test_stop event is fired """ + class TestUser(User): @task def my_task(self): @@ -1096,6 +1108,7 @@ def test_stop_event_quit(self): """ Tests that test_stop event is fired when quit() is called directly """ + class TestUser(User): @task def my_task(self): @@ -1147,6 +1160,7 @@ def test_spawn_uneven_locusts(self): Tests that we can accurately spawn a certain number of locusts, even if it's not an even number of the connected workers """ + class TestUser(User): @task def my_task(self): @@ -2033,6 +2047,7 @@ def test_stop_timeout_with_ramp_down(self): It is expected that the excess users will be stopped as soon as possible in parallel (while respecting the stop_timeout). """ + class MyTaskSet(TaskSet): @task def my_task(self): @@ -2049,12 +2064,18 @@ class MyTestUser(User): runner.start(10, 10, wait=False) runner.spawning_greenlet.join() delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta) - self.assertTrue(runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) + self.assertTrue( + 0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta + ) + self.assertTrue( + runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count + ) ts = time.time() runner.start(2, 4, wait=False) runner.spawning_greenlet.join() delta = time.time() - ts self.assertTrue(1 <= delta <= 1.01, "Expected user count to decrease to 2 in 1s, instead it took %f" % delta) - self.assertTrue(runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count) + self.assertTrue( + runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count + ) From 8804748b28cc0390a126d867233e5dc7e29e79b9 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 16:45:37 -0500 Subject: [PATCH 019/139] Set master state to "spawning" before waiting for dispatch greenlets --- locust/runners.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 7c02998f31..903c45255c 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -603,10 +603,9 @@ def start(self, user_count: int, spawn_rate: float, **kwargs): ) ) logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) + self.update_state(STATE_SPAWNING) dispatch_greenlets.join() - self.update_state(STATE_SPAWNING) - def stop(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: logger.debug("Stopping...") From a1583e7b8f53a0f7cc0cb2d16bcf663ba73ae5ea Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 16:46:04 -0500 Subject: [PATCH 020/139] Add distributed shape integration test (with stop_timeout) --- locust/test/test_runners.py | 223 ++++++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 8521929987..e6f941ccb1 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -608,6 +608,7 @@ def tick(self): # Give workers time to connect sleep(0.1) + # Start a shape test master.start_shape() sleep(1) @@ -615,19 +616,241 @@ def tick(self): # Ensure workers have connected and started the correct amount of users for worker in workers: self.assertEqual(3, worker.user_count, "Shape test has not reached stage 1") + # Ensure new stage with more users has been reached sleep(2) for worker in workers: self.assertEqual(7, worker.user_count, "Shape test has not reached stage 2") + # Ensure new stage with less users has been reached sleep(2) for worker in workers: self.assertEqual(1, worker.user_count, "Shape test has not reached stage 3") + # Ensure test stops at the end sleep(2) for worker in workers: self.assertEqual(0, worker.user_count, "Shape test has not stopped") + self.assertEqual("stopped", master.state) + + def test_distributed_shape_with_stop_timeout(self): + """ + Full integration test that starts both a MasterRunner and five WorkerRunner instances + and tests a basic LoadTestShape with scaling up and down users + """ + + class TestUser1(User): + def start(self, group: Group): + gevent.sleep(0.1) + return super().start(group) + + @task + def my_task(self): + gevent.sleep(60) + + class TestUser2(User): + def start(self, group: Group): + gevent.sleep(0.1) + return super().start(group) + + @task + def my_task(self): + gevent.sleep(15) + + class TestUser3(User): + def start(self, group: Group): + gevent.sleep(0.1) + return super().start(group) + + @task + def my_task(self): + gevent.sleep(5) + + class TestShape(LoadTestShape): + def tick(self): + run_time = self.get_run_time() + if run_time < 10: + return 5, 1 + elif run_time < 20: + return 10, 1 + elif run_time < 30: + return 15, 5 + elif run_time < 60: + return 5, 1 + else: + return None + + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + master_env = Environment( + user_classes=[TestUser1, TestUser2, TestUser3], shape_class=TestShape(), stop_timeout=20 + ) + master_env.shape_class.reset_time() + master = master_env.create_master_runner("*", 0) + + workers = [] + for i in range(5): + worker_env = Environment(user_classes=[TestUser1, TestUser2, TestUser3]) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) + workers.append(worker) + + # Give workers time to connect + sleep(0.1) + + self.assertEqual("ready", master.state) + + # Start a shape test + master.start_shape() + sleep(0.1) + + # First stage + self.assertEqual("spawning", master.state) + sleep(5) # runtime = 5s + self.assertEqual("running", master.state) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + sleep(5) # runtime = 10s + + # Second stage + self.assertEqual("spawning", master.state) + sleep(5) # runtime = 15s + self.assertEqual("running", master.state) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + sleep(5) # runtime = 20s + + # Third stage + self.assertEqual("spawning", master.state) + sleep(5) # runtime = 25s + self.assertEqual("running", master.state) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + sleep(5) # runtime = 30s + + # Forth stage + self.assertEqual("spawning", master.state) + sleep(5) # runtime = 35s + + # Forth stage - Excess TestUser3 have been stopped but + # TestUser1/TestUser2 have not reached stop timeout yet, so + # their number are unchanged + self.assertEqual("spawning", master.state) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + sleep(10) # runtime = 45s + + # Forth stage - TestUser2/TestUser3 are now at the desired + # number, but TestUser1 is still unchanged + self.assertEqual("spawning", master.state) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w5 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + sleep(5) # runtime = 50s + + # Forth stage - All users are now at the desired number + self.assertEqual("running", master.state) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + sleep(10) # runtime = 60s + + # Sleep stop_timeout and make sure the test has stopped + sleep(1) # runtime = 61s + self.assertEqual("stopping", master.state) + sleep(20) # runtime = 81s + self.assertEqual("stopped", master.state) + w1 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w2 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + self.assertDictEqual(w1, workers[0].user_class_occurrences) + self.assertDictEqual(w2, workers[1].user_class_occurrences) + self.assertDictEqual(w3, workers[2].user_class_occurrences) + self.assertDictEqual(w4, workers[3].user_class_occurrences) + self.assertDictEqual(w5, workers[4].user_class_occurrences) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + def test_distributed_shape_stop_and_restart(self): """ Test stopping and then restarting a LoadTestShape From 2724a32a5297dd4eb26041c87bfdf4866014aea7 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 17:00:46 -0500 Subject: [PATCH 021/139] Simplify type hint for _create_runner method --- locust/env.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/locust/env.py b/locust/env.py index 4cf165d61f..6d7d851b0e 100644 --- a/locust/env.py +++ b/locust/env.py @@ -1,3 +1,10 @@ +from typing import ( + Dict, + List, + Type, + TypeVar, +) + from .event import Events from .exception import RunnerAlreadyExistsError from .stats import RequestStats @@ -6,12 +13,9 @@ from .user import User from .user.task import filter_tasks_by_tags from .shape import LoadTestShape -from typing import ( - Dict, - List, - Type, - Union, -) + + +RunnerType = TypeVar("RunnerType", bound=Runner) class Environment: @@ -102,13 +106,13 @@ def __init__( def _create_runner( self, - runner_class: Union[Type[LocalRunner], Type[MasterRunner], Type[WorkerRunner]], + runner_class: Type[RunnerType], *args, **kwargs, - ) -> Union[LocalRunner, MasterRunner, WorkerRunner]: + ) -> RunnerType: if self.runner is not None: raise RunnerAlreadyExistsError("Environment.runner already exists (%s)" % self.runner) - self.runner: Union[LocalRunner, MasterRunner, WorkerRunner] = runner_class(self, *args, **kwargs) + self.runner: RunnerType = runner_class(self, *args, **kwargs) return self.runner def create_local_runner(self) -> LocalRunner: From cc190737c1dbc3259c64796dc8a383390d5d7885 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 17:19:25 -0500 Subject: [PATCH 022/139] Make input_listener more flexible --- locust/input_events.py | 28 +++++++++++++++++----------- locust/main.py | 1 + 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/locust/input_events.py b/locust/input_events.py index 619fad936c..7a212d5ebb 100644 --- a/locust/input_events.py +++ b/locust/input_events.py @@ -96,7 +96,9 @@ def get_poller(): return UnixKeyPoller() -def input_listener(key_to_user_params: Dict[str, Tuple[int, float]], runner: Runner): +def input_listener( + key_to_user_params: Dict[str, Tuple[int, float]], key_to_callbacks: Dict[str, callable], runner: Runner +): def input_listener_func(): try: poller = get_poller() @@ -112,16 +114,20 @@ def input_listener_func(): try: user_params = key_to_user_params[input] except KeyError: - continue - user_delta, spawn_rate = user_params - if runner.state == STATE_SPAWNING and user_delta > 0: - logging.warning("Already spawning users, can't spawn more right now") - continue - elif runner.state == STATE_SPAWNING and user_delta < 0: - logging.warning("Spawning users, can't stop right now") - continue - user_count = max(0, user_count + user_delta) - runner.start(user_count, spawn_rate) + try: + key_to_callbacks[input](runner) + except KeyError: + continue + else: + user_delta, spawn_rate = user_params + if runner.state == STATE_SPAWNING and user_delta > 0: + logging.warning("Already spawning users, can't spawn more right now") + continue + elif runner.state == STATE_SPAWNING and user_delta < 0: + logging.warning("Spawning users, can't stop right now") + continue + user_count = max(0, user_count + user_delta) + runner.start(user_count, spawn_rate) else: gevent.sleep(0.2) except Exception as e: diff --git a/locust/main.py b/locust/main.py index e2a2b08ecf..e1e119dde0 100644 --- a/locust/main.py +++ b/locust/main.py @@ -356,6 +356,7 @@ def timelimit_stop(): "s": (-1, 100), "S": (-10, 100), }, + {}, runner, ) ) From 681049457f5d57cca99b61df4c00d170e5e23fcd Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 17:19:56 -0500 Subject: [PATCH 023/139] Update state before dispatching + fix flaky test --- locust/runners.py | 3 ++- locust/test/test_runners.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 903c45255c..bae4ee4d59 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -580,6 +580,8 @@ def start(self, user_count: int, spawn_rate: float, **kwargs): self.exceptions = {} self.environment.events.test_start.fire(environment=self.environment) + self.update_state(STATE_SPAWNING) + users_dispatcher = dispatch_users( worker_nodes=self.clients.ready + self.clients.running + self.clients.spawning, user_class_occurrences=self.target_user_class_occurrences, @@ -603,7 +605,6 @@ def start(self, user_count: int, spawn_rate: float, **kwargs): ) ) logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) - self.update_state(STATE_SPAWNING) dispatch_greenlets.join() def stop(self): diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index e6f941ccb1..c85fc9f888 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -642,7 +642,7 @@ def test_distributed_shape_with_stop_timeout(self): class TestUser1(User): def start(self, group: Group): - gevent.sleep(0.1) + gevent.sleep(0.5) return super().start(group) @task @@ -651,7 +651,7 @@ def my_task(self): class TestUser2(User): def start(self, group: Group): - gevent.sleep(0.1) + gevent.sleep(0.5) return super().start(group) @task @@ -660,7 +660,7 @@ def my_task(self): class TestUser3(User): def start(self, group: Group): - gevent.sleep(0.1) + gevent.sleep(0.5) return super().start(group) @task From 67a1dd2ef8b86fe8ac5d1ed43fe1908575e7df62 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 17:23:36 -0500 Subject: [PATCH 024/139] Ensure key presses are unique across both dictionaries --- locust/input_events.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/locust/input_events.py b/locust/input_events.py index 7a212d5ebb..50297940d4 100644 --- a/locust/input_events.py +++ b/locust/input_events.py @@ -99,6 +99,9 @@ def get_poller(): def input_listener( key_to_user_params: Dict[str, Tuple[int, float]], key_to_callbacks: Dict[str, callable], runner: Runner ): + # Ensure key presses are unique across both dictionaries + assert len(set(key_to_user_params.keys()).intersection(key_to_callbacks.keys())) == 0 + def input_listener_func(): try: poller = get_poller() From 62953c1d02b3a2b72e663055bbdf1a3c9045e9ba Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 17:26:46 -0500 Subject: [PATCH 025/139] Rephrase comment --- locust/test/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index 3b77202224..ac7a57491b 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -359,7 +359,7 @@ def t(self): stdin.write(b"S") gevent.sleep(1) - # These two should not do anything since we are already at zero users + # This should not do anything since we are already at zero users stdin.write(b"S") gevent.sleep(1) From 00eee4802899eea0cf456cb8137039de6483d314 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 18:14:13 -0500 Subject: [PATCH 026/139] Add doc strings --- locust/dispatch.py | 33 ++++++++++++++++++++++++++++++--- locust/distribution.py | 11 +++++++++-- locust/test/test_runners.py | 4 ++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index d3de20a858..9c34938ed0 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -20,6 +20,31 @@ def dispatch_users( user_class_occurrences: Dict[str, int], spawn_rate: float, ) -> Generator[Dict[str, Dict[str, int]], None, None]: + """ + Generator function that dispatches the users + in `user_class_occurrences` to the workers. + The currently running users is also taken into + account. + + It waits an appropriate amount of time between each iteration + in order for the spawn rate to be respected, whether running in + local or distributed mode. + + The spawn rate is only applicable when additional users are needed. + Hence, if `user_class_occurrences` contains less users than there are + currently running, this function won't wait and will only run for + one iteration. The logic for not stopping users at a rate of `spawn_rate` + is that stopping them is a blocking operation, especially when + having a `stop_timeout` and users with tasks running for a few seconds or + more. If we were to dispatch multiple spawn messages to have a ramp down, + we'd run into problems where the previous spawning would be killed + by the new message. See the call to `self.spawning_greenlet.kill()` in + `:py:meth:`locust.runners.LocalRunner.start` and `:py:meth:`locust.runners.WorkerRunner.worker`. + + :param worker_nodes: List of worker nodes + :param user_class_occurrences: Desired number of users for each class + :param spawn_rate: The spawn rate + """ initial_dispatched_users = { worker_node.id: { user_class: worker_node.user_class_occurrences.get(user_class, 0) @@ -119,10 +144,8 @@ def dispatch_users( gevent.sleep(max(0.0, wait_between_dispatch - delta)) # If we are here, it means we have an excess of users for one or more user classes. - # Hence, we need to dispatch a last set of users that will bring the desired users + # Hence, we need to dispatch a last set of users that will bring the users # distribution to the desired one. - # TODO: Explain why we don't stop the users at "spawn_rate" - # and why we stop the excess users once at the end. yield balanced_users @@ -188,6 +211,10 @@ def balance_users_among_workers( worker_nodes, # type: List[WorkerNode] user_class_occurrences: Dict[str, int], ) -> Dict[str, Dict[str, int]]: + """ + Balance the users among the workers so that + each worker gets around the same number of users of each user class + """ balanced_users = { worker_node.id: {user_class: 0 for user_class in sorted(user_class_occurrences.keys())} for worker_node in worker_nodes diff --git a/locust/distribution.py b/locust/distribution.py index 3acdfae239..05aff62b0e 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -14,11 +14,18 @@ def weight_users( number_of_users: int, ) -> Dict[str, int]: """ - Compute users to spawn + Compute the desired state of users using the weight of each user class. + + If `number_of_users` is less than `len(user_classes)`, at most one user of each user class + is chosen. User classes with higher weight are chosen first. + + If `number_of_users` is greater than or equal to `len(user_classes)`, at least one user of each + user class will be chosen. The greater `number_of_users` is, the better the actual distribution + of users will match the desired one (as dictated by the weight attributes). :param user_classes: the list of user class :param number_of_users: total number of users - :return: the new set of users to run + :return: the set of users to run """ assert number_of_users >= 0 diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index c85fc9f888..e426721a31 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -390,7 +390,7 @@ def my_task(self): runner.spawning_greenlet.join() delta = time.time() - ts self.assertTrue( - 0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta + 0 <= delta <= 0.05, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta ) self.assertTrue( runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count @@ -401,7 +401,7 @@ def my_task(self): runner.spawning_greenlet.join() delta = time.time() - ts self.assertTrue( - 0 <= delta <= 0.01, "Expected user count to decrease to 2 instantaneously, instead it took %f" % delta + 0 <= delta <= 0.05, "Expected user count to decrease to 2 instantaneously, instead it took %f" % delta ) self.assertTrue( runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count From da310d5955eb3ccfe5f1357b547fdffbcbfd2d07 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 17 Nov 2020 18:52:45 -0500 Subject: [PATCH 027/139] Fix typo --- locust/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/runners.py b/locust/runners.py index bae4ee4d59..891e5c5d3e 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -811,7 +811,7 @@ def on_user_error(user_instance, exception, tb): self.environment.events.user_error.add_listener(on_user_error) def start(self, user_count, spawn_rate, wait=False): - raise NotImplementedError("user start_worker") + raise NotImplementedError("use start_worker") def start_worker(self, user_class_occurrences: Dict[str, int], **kwargs): """ From 82f4d91c24962fc0d04a898757d5f7ec7360a3ff Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 16:05:24 -0500 Subject: [PATCH 028/139] Simplify users distribution algorithm (recursion is no longer used) --- locust/distribution.py | 87 +++++++++++++------------------- locust/test/test_distribution.py | 4 +- 2 files changed, 37 insertions(+), 54 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 05aff62b0e..912db25607 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -1,4 +1,5 @@ import math +from itertools import combinations_with_replacement from operator import attrgetter from typing import ( Dict, @@ -59,82 +60,64 @@ def weight_users( return user_class_occurrences elif sum(user_class_occurrences.values()) > number_of_users: - user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} - _recursive_remove_users( + return _find_ideal_users_to_remove( user_classes, - number_of_users, + sum(user_class_occurrences.values()) - number_of_users, user_class_occurrences.copy(), - user_class_occurrences_candidates, ) - return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] elif sum(user_class_occurrences.values()) < number_of_users: - user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} - _recursive_add_users( + return _find_ideal_users_to_add( user_classes, - number_of_users, + number_of_users - sum(user_class_occurrences.values()), user_class_occurrences.copy(), - user_class_occurrences_candidates, ) - return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] -def _recursive_add_users( +def _find_ideal_users_to_add( user_classes: List[Type[User]], - number_of_users: int, - user_class_occurrences_candidate: Dict[str, int], - user_class_occurrences_candidates: Dict[float, Dict[str, int]], -): - if sum(user_class_occurrences_candidate.values()) == number_of_users: + number_of_users_to_add: int, + user_class_occurrences: Dict[str, int], +) -> Dict[str, int]: + user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} + + for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add): + user_class_occurrences_candidate = { + user_class.__name__: user_class_occurrences[user_class.__name__] + + sum(1 for user_class_ in user_classes_combination if user_class_.__name__ == user_class.__name__) + for user_class in user_classes + } distance = distance_from_desired_distribution( user_classes, user_class_occurrences_candidate, ) if distance not in user_class_occurrences_candidates: - user_class_occurrences_candidates[distance] = user_class_occurrences_candidate - return - elif sum(user_class_occurrences_candidate.values()) > number_of_users: - return - - for user_class in user_classes: - user_class_occurrences_candidate_ = user_class_occurrences_candidate.copy() - user_class_occurrences_candidate_[user_class.__name__] += 1 - _recursive_add_users( - user_classes, - number_of_users, - user_class_occurrences_candidate_, - user_class_occurrences_candidates, - ) + user_class_occurrences_candidates[distance] = user_class_occurrences_candidate.copy() + + return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] -def _recursive_remove_users( +def _find_ideal_users_to_remove( user_classes: List[Type[User]], - number_of_users: int, - user_class_occurrences_candidate: Dict[str, int], - user_class_occurrences_candidates: Dict[float, Dict[str, int]], -): - if sum(user_class_occurrences_candidate.values()) == number_of_users: + number_of_users_to_remove: int, + user_class_occurrences: Dict[str, int], +) -> Dict[str, int]: + user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} + + for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_remove): + user_class_occurrences_candidate = { + user_class.__name__: user_class_occurrences[user_class.__name__] + - sum(1 for user_class_ in user_classes_combination if user_class_.__name__ == user_class.__name__) + for user_class in user_classes + } distance = distance_from_desired_distribution( user_classes, user_class_occurrences_candidate, ) if distance not in user_class_occurrences_candidates: - user_class_occurrences_candidates[distance] = user_class_occurrences_candidate - return - elif sum(user_class_occurrences_candidate.values()) < number_of_users: - return - - for user_class in sorted(user_classes, key=lambda u: u.__name__, reverse=True): - if user_class_occurrences_candidate[user_class.__name__] == 1: - continue - user_class_occurrences_candidate_ = user_class_occurrences_candidate.copy() - user_class_occurrences_candidate_[user_class.__name__] -= 1 - _recursive_remove_users( - user_classes, - number_of_users, - user_class_occurrences_candidate_, - user_class_occurrences_candidates, - ) + user_class_occurrences_candidates[distance] = user_class_occurrences_candidate.copy() + + return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] def distance_from_desired_distribution( diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 7614cadac5..3d55b7d643 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -74,7 +74,7 @@ class User3(User): user_classes=[User1, User2, User3], number_of_users=5, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 2, "User3": 1}) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 2}) user_class_occurrences = weight_users( user_classes=[User1, User2, User3], @@ -204,7 +204,7 @@ class User3(User): user_classes=[User1, User2, User3], number_of_users=4, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 1}) + self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 2}) user_class_occurrences = weight_users( user_classes=[User1, User2, User3], From eb14c9b4a109683bd113d40bffb51ea21fa9aa54 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 16:21:10 -0500 Subject: [PATCH 029/139] Further simplify users distribution code --- locust/distribution.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 912db25607..7d03576f0c 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -59,15 +59,8 @@ def weight_users( if sum(user_class_occurrences.values()) == number_of_users: return user_class_occurrences - elif sum(user_class_occurrences.values()) > number_of_users: - return _find_ideal_users_to_remove( - user_classes, - sum(user_class_occurrences.values()) - number_of_users, - user_class_occurrences.copy(), - ) - - elif sum(user_class_occurrences.values()) < number_of_users: - return _find_ideal_users_to_add( + else: + return _find_ideal_users_to_add_or_remove( user_classes, number_of_users - sum(user_class_occurrences.values()), user_class_occurrences.copy(), @@ -97,17 +90,21 @@ def _find_ideal_users_to_add( return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] -def _find_ideal_users_to_remove( +def _find_ideal_users_to_add_or_remove( user_classes: List[Type[User]], - number_of_users_to_remove: int, + number_of_users_to_add_or_remove: int, user_class_occurrences: Dict[str, int], ) -> Dict[str, int]: + sign = -1 if number_of_users_to_add_or_remove < 0 else 1 + + number_of_users_to_add_or_remove = abs(number_of_users_to_add_or_remove) + user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} - for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_remove): + for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add_or_remove): user_class_occurrences_candidate = { user_class.__name__: user_class_occurrences[user_class.__name__] - - sum(1 for user_class_ in user_classes_combination if user_class_.__name__ == user_class.__name__) + + sign * sum(1 for user_class_ in user_classes_combination if user_class_.__name__ == user_class.__name__) for user_class in user_classes } distance = distance_from_desired_distribution( From f82313842465e546e1bde0b15eb6b370756af4ea Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 16:40:49 -0500 Subject: [PATCH 030/139] Remove unused function --- locust/distribution.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 7d03576f0c..1f7403dcd6 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -67,29 +67,6 @@ def weight_users( ) -def _find_ideal_users_to_add( - user_classes: List[Type[User]], - number_of_users_to_add: int, - user_class_occurrences: Dict[str, int], -) -> Dict[str, int]: - user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} - - for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add): - user_class_occurrences_candidate = { - user_class.__name__: user_class_occurrences[user_class.__name__] - + sum(1 for user_class_ in user_classes_combination if user_class_.__name__ == user_class.__name__) - for user_class in user_classes - } - distance = distance_from_desired_distribution( - user_classes, - user_class_occurrences_candidate, - ) - if distance not in user_class_occurrences_candidates: - user_class_occurrences_candidates[distance] = user_class_occurrences_candidate.copy() - - return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] - - def _find_ideal_users_to_add_or_remove( user_classes: List[Type[User]], number_of_users_to_add_or_remove: int, From 8750bf9f2e3198e658f3f5cce3c8daa44f3c9f76 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 16:41:44 -0500 Subject: [PATCH 031/139] Restore former input_listener signature & logic --- locust/input_events.py | 28 ++++------------------------ locust/main.py | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/locust/input_events.py b/locust/input_events.py index 50297940d4..c6a8f05aab 100644 --- a/locust/input_events.py +++ b/locust/input_events.py @@ -96,12 +96,7 @@ def get_poller(): return UnixKeyPoller() -def input_listener( - key_to_user_params: Dict[str, Tuple[int, float]], key_to_callbacks: Dict[str, callable], runner: Runner -): - # Ensure key presses are unique across both dictionaries - assert len(set(key_to_user_params.keys()).intersection(key_to_callbacks.keys())) == 0 - +def input_listener(key_to_func_map: Dict[str, callable]): def input_listener_func(): try: poller = get_poller() @@ -109,28 +104,13 @@ def input_listener_func(): logging.info(e) return - user_count = 0 try: while True: input = poller.poll() if input: - try: - user_params = key_to_user_params[input] - except KeyError: - try: - key_to_callbacks[input](runner) - except KeyError: - continue - else: - user_delta, spawn_rate = user_params - if runner.state == STATE_SPAWNING and user_delta > 0: - logging.warning("Already spawning users, can't spawn more right now") - continue - elif runner.state == STATE_SPAWNING and user_delta < 0: - logging.warning("Spawning users, can't stop right now") - continue - user_count = max(0, user_count + user_delta) - runner.start(user_count, spawn_rate) + for key in key_to_func_map: + if input == key: + key_to_func_map[key]() else: gevent.sleep(0.2) except Exception as e: diff --git a/locust/main.py b/locust/main.py index e1e119dde0..6521ab1051 100644 --- a/locust/main.py +++ b/locust/main.py @@ -351,13 +351,19 @@ def timelimit_stop(): input_listener_greenlet = gevent.spawn( input_listener( { - "w": (1, 100), - "W": (10, 100), - "s": (-1, 100), - "S": (-10, 100), + "w": lambda: runner.start(runner.user_count + 1, 100) + if runner.state != "spawning" + else logging.warning("Already spawning users, can't spawn more right now"), + "W": lambda: runner.start(runner.user_count + 10, 100) + if runner.state != "spawning" + else logging.warning("Already spawning users, can't spawn more right now"), + "s": lambda: runner.start(max(0, runner.user_count - 1), 100) + if runner.state != "spawning" + else logging.warning("Spawning users, can't stop right now"), + "S": lambda: runner.start(max(0, runner.user_count - 10), 100) + if runner.state != "spawning" + else logging.warning("Spawning users, can't stop right now"), }, - {}, - runner, ) ) input_listener_greenlet.link_exception(greenlet_exception_handler) From e9c8fae595bc2b1fb0dd46da8030fd02c0e41b47 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 16:42:09 -0500 Subject: [PATCH 032/139] Add comments and doctrings --- locust/dispatch.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 9c34938ed0..dd27affd3d 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -7,6 +7,7 @@ Generator, List, TYPE_CHECKING, + Tuple, ) import gevent @@ -45,6 +46,7 @@ def dispatch_users( :param user_class_occurrences: Desired number of users for each class :param spawn_rate: The spawn rate """ + # This represents the already running users among the workers initial_dispatched_users = { worker_node.id: { user_class: worker_node.user_class_occurrences.get(user_class, 0) @@ -53,13 +55,13 @@ def dispatch_users( for worker_node in worker_nodes } - # This represents the desired users distribution + # This represents the desired users distribution among the workers balanced_users = balance_users_among_workers( worker_nodes, user_class_occurrences, ) - # This represents the desired users distribution minus the already running users + # This represents the desired users distribution minus the already running users among the workers effective_balanced_users = { worker_node.id: { user_class: max( @@ -169,7 +171,11 @@ def distribute_current_user_class_among_workers( user_class: str, number_of_users_in_current_dispatch: int, number_of_users_per_dispatch: int, -): +) -> Tuple[bool, int]: + """ + :return done: boolean indicating if we have enough users to perform a dispatch to the workers + :return number_of_users_in_current_dispatch: current number of users in the dispatch + """ done = False for worker_node_id in itertools.cycle(effective_balanced_users.keys()): if effective_balanced_users[worker_node_id][user_class] == 0: From b4386b9e52fabc8fc8449971112a73cbccbc6c47 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 16:49:21 -0500 Subject: [PATCH 033/139] Remove unused imports --- locust/input_events.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/locust/input_events.py b/locust/input_events.py index c6a8f05aab..0d346fe0af 100644 --- a/locust/input_events.py +++ b/locust/input_events.py @@ -1,18 +1,10 @@ -from typing import ( - Dict, - Tuple, -) +from typing import Dict import gevent import logging import os import sys -from locust.runners import ( - Runner, - STATE_SPAWNING, -) - if os.name == "nt": from win32api import STD_INPUT_HANDLE from win32console import ( From 0b1ae30f4747e5ab27f70b1daca2686f133e5fb2 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:07:17 -0500 Subject: [PATCH 034/139] Fix test --- locust/test/test_runners.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index e426721a31..38f58510a3 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -708,7 +708,7 @@ def tick(self): sleep(5) # runtime = 5s self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} @@ -775,7 +775,7 @@ def tick(self): # their number are unchanged self.assertEqual("spawning", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} @@ -795,7 +795,7 @@ def tick(self): # number, but TestUser1 is still unchanged self.assertEqual("spawning", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} w5 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} @@ -814,7 +814,7 @@ def tick(self): # Forth stage - All users are now at the desired number self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} @@ -1607,7 +1607,7 @@ def my_task(self): server.mocked_send(Message("client_ready", None, "fake_client1")) master.start(7, 7) - self.assertEqual({"MyUser1": 4, "MyUser2": 3}, master.target_user_class_occurrences) + self.assertEqual({"MyUser1": 3, "MyUser2": 4}, master.target_user_class_occurrences) self.assertEqual(7, master.target_user_count) self.assertEqual(7, master.spawn_rate) @@ -1638,7 +1638,7 @@ def my_task(self): server.mocked_send(Message("client_ready", None, "fake_client1")) master.start(7, 7) - self.assertEqual({"MyUser1": 4, "MyUser2": 3}, master.target_user_class_occurrences) + self.assertEqual({"MyUser1": 3, "MyUser2": 4}, master.target_user_class_occurrences) self.assertEqual(7, master.target_user_count) self.assertEqual(7, master.spawn_rate) From 34755fce1c6fa5700e4b2f13ec643c8fcfdd7332 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:07:31 -0500 Subject: [PATCH 035/139] Remove unused code --- locust/test/test_dispatch.py | 18 ------------------ locust/test/test_distribution.py | 2 -- 2 files changed, 20 deletions(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 15dd94c3a3..78d118ccb3 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -12,8 +12,6 @@ class TestBalanceUsersAmongWorkers(unittest.TestCase): - maxDiff = None - def test_balance_users_among_1_worker(self): worker_node1 = WorkerNode("1") @@ -109,8 +107,6 @@ def test_balance_users_among_3_workers(self): class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): - maxDiff = None - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): worker_node1 = WorkerNode("1") worker_node2 = WorkerNode("2") @@ -773,8 +769,6 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): - maxDiff = None - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): worker_node1 = WorkerNode("1") worker_node1.user_class_occurrences = {} @@ -1353,8 +1347,6 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): - maxDiff = None - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): worker_node1 = WorkerNode("1") worker_node1.user_class_occurrences = {} @@ -1705,8 +1697,6 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): - maxDiff = None - def test_dispatch_users_to_3_workers(self): worker_node1 = WorkerNode("1") worker_node1.user_class_occurrences = {"User3": 15} @@ -1741,8 +1731,6 @@ def test_dispatch_users_to_3_workers(self): class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): - maxDiff = None - def test_dispatch_users_to_3_workers(self): worker_node1 = WorkerNode("1") worker_node1.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} @@ -1777,8 +1765,6 @@ def test_dispatch_users_to_3_workers(self): class TestNumberOfUsersLeftToDispatch(unittest.TestCase): - maxDiff = None - def test_number_of_users_left_to_dispatch(self): user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} balanced_users = { @@ -1816,8 +1802,6 @@ def test_number_of_users_left_to_dispatch(self): class AllUsersHaveBeenDispatched(unittest.TestCase): - maxDiff = None - def test_all_users_have_been_dispatched(self): user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} effective_balanced_users = { @@ -1867,8 +1851,6 @@ def test_all_users_have_been_dispatched(self): class TestAllUsersOfCurrentClassHaveBeenDispatched(unittest.TestCase): - maxDiff = None - def test_all_users_of_current_class_have_been_dispatched(self): effective_balanced_users = { "Worker1": {"User1": 3, "User2": 1, "User3": 4}, diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 3d55b7d643..c34467c0d2 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -5,8 +5,6 @@ class TestDistribution(unittest.TestCase): - maxDiff = None - def test_distribution_no_user_classes(self): user_class_occurrences = weight_users( user_classes=[], From 0fd881b6b4f26f446ad29a13a44af46ad5f5bfac Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:14:35 -0500 Subject: [PATCH 036/139] Relax pass criteria --- locust/test/test_dispatch.py | 246 +++++++++++++++++------------------ 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 78d118ccb3..74b147093d 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -130,7 +130,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -142,7 +142,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -154,7 +154,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -166,7 +166,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -178,7 +178,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -190,7 +190,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -202,7 +202,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -214,7 +214,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -226,12 +226,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") @@ -254,7 +254,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -266,7 +266,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -278,7 +278,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -290,7 +290,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -302,7 +302,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -314,7 +314,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -326,7 +326,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -338,7 +338,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -350,12 +350,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -378,7 +378,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -390,7 +390,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -402,7 +402,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -414,7 +414,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -426,7 +426,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -438,7 +438,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -450,7 +450,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -462,7 +462,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -474,12 +474,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -502,7 +502,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -514,7 +514,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -526,7 +526,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -538,7 +538,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -550,12 +550,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") @@ -580,7 +580,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -592,7 +592,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -604,7 +604,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -616,7 +616,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -628,12 +628,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -656,7 +656,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -668,7 +668,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -680,12 +680,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -708,7 +708,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -720,7 +720,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -732,12 +732,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -760,12 +760,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): @@ -795,7 +795,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -807,7 +807,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -819,7 +819,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -831,7 +831,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -843,7 +843,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -855,7 +855,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -867,12 +867,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") @@ -898,7 +898,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -910,7 +910,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -922,7 +922,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -934,7 +934,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -946,7 +946,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -958,7 +958,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -970,12 +970,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -1001,7 +1001,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1013,7 +1013,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1025,7 +1025,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1037,7 +1037,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1049,7 +1049,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1061,7 +1061,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1073,12 +1073,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -1104,7 +1104,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1116,7 +1116,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1128,7 +1128,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1140,12 +1140,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") @@ -1173,7 +1173,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1185,7 +1185,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1197,7 +1197,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1209,12 +1209,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -1240,7 +1240,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1252,7 +1252,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1264,12 +1264,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -1295,7 +1295,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1307,12 +1307,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -1338,12 +1338,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): @@ -1373,7 +1373,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1385,7 +1385,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1397,12 +1397,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") @@ -1428,7 +1428,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1440,7 +1440,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertDictEqual( @@ -1452,12 +1452,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(1.99 <= delta <= 2.01, delta) + self.assertTrue(1.98 <= delta <= 2.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -1483,7 +1483,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1495,7 +1495,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertDictEqual( @@ -1507,12 +1507,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -1538,7 +1538,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1550,12 +1550,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.99 <= delta <= 1.01, delta) + self.assertTrue(0.98 <= delta <= 1.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") @@ -1583,7 +1583,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1595,12 +1595,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.01 <= delta <= sleep_time + 0.01, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -1626,12 +1626,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -1657,12 +1657,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -1688,12 +1688,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): @@ -1722,12 +1722,12 @@ def test_dispatch_users_to_3_workers(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): @@ -1756,12 +1756,12 @@ def test_dispatch_users_to_3_workers(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.01, delta) + self.assertTrue(0 <= delta <= 0.02, delta) class TestNumberOfUsersLeftToDispatch(unittest.TestCase): From 04485c7dcd52424e076bc69feb2958663e4f962c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:15:07 -0500 Subject: [PATCH 037/139] Remove unused condition in elif --- locust/dispatch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index dd27affd3d..25460bb95b 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -113,8 +113,7 @@ def dispatch_users( gevent.sleep(max(0.0, wait_between_dispatch - delta)) elif ( - not less_users_than_desired - and number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) <= number_of_users_per_dispatch ): yield balanced_users From b593a615b5ed44fc6bdbe9e97798dc6e077e624e Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:28:26 -0500 Subject: [PATCH 038/139] Remove duplicated test --- locust/test/test_runners.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 38f58510a3..778cc1b149 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1621,37 +1621,6 @@ def my_task(self): self.assertEqual(1, master.target_user_count) self.assertEqual(3, master.spawn_rate) - def test_user_class_occurrences(self): - class MyUser1(User): - @task - def my_task(self): - pass - - class MyUser2(User): - @task - def my_task(self): - pass - - with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: - master = self.get_runner(user_classes=[MyUser1, MyUser2]) - - server.mocked_send(Message("client_ready", None, "fake_client1")) - - master.start(7, 7) - self.assertEqual({"MyUser1": 3, "MyUser2": 4}, master.target_user_class_occurrences) - self.assertEqual(7, master.target_user_count) - self.assertEqual(7, master.spawn_rate) - - master.start(10, 10) - self.assertEqual({"MyUser1": 5, "MyUser2": 5}, master.target_user_class_occurrences) - self.assertEqual(10, master.target_user_count) - self.assertEqual(10, master.spawn_rate) - - master.start(1, 3) - self.assertEqual({"MyUser1": 1, "MyUser2": 0}, master.target_user_class_occurrences) - self.assertEqual(1, master.target_user_count) - self.assertEqual(3, master.spawn_rate) - class TestWorkerRunner(LocustTestCase): def setUp(self): From 15730f09fd1088cc402be3b252ec15b93e997ca6 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:37:07 -0500 Subject: [PATCH 039/139] Combine two tests into a single test --- locust/test/test_runners.py | 53 ++----------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 778cc1b149..38cc3034a9 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1863,7 +1863,7 @@ def my_task(self): self.assertEqual(9, len(worker.user_greenlets)) worker.quit() - def test_attributes_populated_when_calling_start_worker(self): + def test_computed_properties(self): class MyUser1(User): wait_time = constant(1) @@ -1896,56 +1896,6 @@ def my_task(self): ) worker.spawning_greenlet.join() self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 10, "MyUser2": 10}) - - client.mocked_send( - Message( - "spawn", - { - "timestamp": 1605538585, - "user_class_occurrences": {"MyUser1": 1, "MyUser2": 2}, - "host": "", - "stop_timeout": None, - }, - "dummy_client_id", - ) - ) - worker.spawning_greenlet.join() - self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) - - worker.quit() - - def test_user_class_occurrences(self): - class MyUser1(User): - wait_time = constant(1) - - @task - def my_task(self): - pass - - class MyUser2(User): - wait_time = constant(1) - - @task - def my_task(self): - pass - - with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: - environment = Environment() - worker = self.get_runner(environment=environment, user_classes=[MyUser1, MyUser2]) - - client.mocked_send( - Message( - "spawn", - { - "timestamp": 1605538584, - "user_class_occurrences": {"MyUser1": 10, "MyUser2": 10}, - "host": "", - "stop_timeout": None, - }, - "dummy_client_id", - ) - ) - worker.spawning_greenlet.join() self.assertDictEqual(worker.target_user_class_occurrences, {"MyUser1": 10, "MyUser2": 10}) self.assertEqual(worker.target_user_count, 20) @@ -1962,6 +1912,7 @@ def my_task(self): ) ) worker.spawning_greenlet.join() + self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) self.assertDictEqual(worker.target_user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) self.assertEqual(worker.target_user_count, 3) From 3381be8f4df81e2f792a76dc6668725bb69b1ec0 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 17:50:53 -0500 Subject: [PATCH 040/139] Rephrasing some comments + add type hints --- locust/runners.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 891e5c5d3e..11cd1caef8 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -208,7 +208,7 @@ def stop_users(self, user_classes_stop_count: Dict[str, int]): user_to_stop: User = to_stop.pop() logger.debug("Stopping %s" % user_to_stop.greenlet.name) if user_to_stop.greenlet is greenlet.getcurrent(): - # User called runner.quit(), so dont block waiting for killing to finish" + # User called runner.quit(), so don't block waiting for killing to finish user_to_stop.group.killone(user_to_stop.greenlet, block=False) elif self.environment.stop_timeout: async_calls_to_stop.add(gevent.spawn_later(0, User.stop, user_to_stop, force=False)) @@ -537,7 +537,7 @@ def on_quitting(environment, **kw): self.environment.events.quitting.add_listener(on_quitting) @property - def user_count(self): + def user_count(self) -> int: return sum(c.user_count for c in self.clients.values()) def cpu_log_warning(self): @@ -547,7 +547,7 @@ def cpu_log_warning(self): warning_emitted = True return warning_emitted - def start(self, user_count: int, spawn_rate: float, **kwargs): + def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: num_workers = len(self.clients.ready) + len(self.clients.running) + len(self.clients.spawning) if not num_workers: logger.warning( @@ -621,7 +621,7 @@ def stop(self): logger.debug("Sending stop message to client %s" % client.id) self.server.send_to_client(Message("stop", None, client.id)) - # Give 60s more for all workers to stop + # Give an additional 60s for all workers to stop timeout = gevent.Timeout(self.environment.stop_timeout or 0 + 60) timeout.start() try: From 8f84ddc6ba8e4b3877b19c03fdbf293b0ddaf46f Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 18 Nov 2020 18:36:48 -0500 Subject: [PATCH 041/139] Bugfix when running in local runner --- locust/runners.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 11cd1caef8..201f455c7e 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -267,8 +267,12 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): self.target_user_class_occurrences = weight_users(self.user_classes, user_count) + # Dummy worker node since dispatch_users needs it + dummy_worker_node = WorkerNode(id="dummy") + dummy_worker_node.user_class_occurrences = self.user_class_occurrences + users_dispatcher = dispatch_users( - worker_nodes=[WorkerNode(id="dummy")], + worker_nodes=[dummy_worker_node], user_class_occurrences=self.target_user_class_occurrences, spawn_rate=spawn_rate, ) @@ -282,7 +286,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): for dispatched_users in users_dispatcher: user_classes_spawn_count = {} user_classes_stop_count = {} - user_class_occurrences = dispatched_users["dummy"] + user_class_occurrences = dispatched_users[dummy_worker_node.id] for user_class, occurrences in user_class_occurrences.items(): logger.debug( "Updating running test with %d users of class %s and wait=%r" % (occurrences, user_class, wait) From 72d6b5d6275fd33888ba21e177a11797b971cbc7 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 25 Nov 2020 20:56:35 -0500 Subject: [PATCH 042/139] Handle self.shape_greenlet = None --- locust/runners.py | 7 +++++-- locust/test/test_runners.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 201f455c7e..92eece7723 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -352,8 +352,11 @@ def stop(self): self.spawning_greenlet.kill(block=True) if self.environment.shape_class is not None and self.shape_greenlet is not greenlet.getcurrent(): - self.shape_greenlet.kill(block=True) - self.shape_greenlet = None + # If the test was not started yet and that locust is + # stopped/quit, shape_greenlet will be None. + if self.shape_greenlet is not None: + self.shape_greenlet.kill(block=True) + self.shape_greenlet = None self.shape_last_state = None self.stop_users(self.user_class_occurrences) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 38cc3034a9..aafe1a64b1 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -698,6 +698,7 @@ def tick(self): sleep(0.1) self.assertEqual("ready", master.state) + self.assertEqual(5, len(master.clients.ready)) # Start a shape test master.start_shape() @@ -705,7 +706,7 @@ def tick(self): # First stage self.assertEqual("spawning", master.state) - sleep(5) # runtime = 5s + sleep(8) # runtime = 8s self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} @@ -722,7 +723,7 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(5) # runtime = 10s + sleep(2) # runtime = 10s # Second stage self.assertEqual("spawning", master.state) From 52e39ffe99fba44dffd5ff7854bac17bb4842f56 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 1 Dec 2020 18:10:12 -0500 Subject: [PATCH 043/139] Sort workers by id --- locust/dispatch.py | 3 +++ locust/test/test_runners.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 25460bb95b..6e6180b15a 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -46,6 +46,9 @@ def dispatch_users( :param user_class_occurrences: Desired number of users for each class :param spawn_rate: The spawn rate """ + # Get repeatable behaviour. + worker_nodes = sorted(worker_nodes, key=lambda w: w.id) + # This represents the already running users among the workers initial_dispatched_users = { worker_node.id: { diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index aafe1a64b1..bf21121743 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -671,13 +671,13 @@ class TestShape(LoadTestShape): def tick(self): run_time = self.get_run_time() if run_time < 10: - return 5, 1 + return 5, 3 elif run_time < 20: - return 10, 1 + return 10, 3 elif run_time < 30: - return 15, 5 + return 15, 3 elif run_time < 60: - return 5, 1 + return 5, 3 else: return None @@ -700,13 +700,18 @@ def tick(self): self.assertEqual("ready", master.state) self.assertEqual(5, len(master.clients.ready)) + # Re-order `workers` so that it is sorted by `id`. + # This is required because the dispatch is done + # on the sorted workers. + workers = sorted(workers, key=lambda w: w.client_id) + # Start a shape test master.start_shape() sleep(0.1) # First stage self.assertEqual("spawning", master.state) - sleep(8) # runtime = 8s + sleep(5) # runtime = 5s self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} @@ -723,7 +728,7 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(2) # runtime = 10s + sleep(5) # runtime = 10s # Second stage self.assertEqual("spawning", master.state) From 80dee44356a33554ddc506739bd093f77eaf6daf Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 9 Dec 2020 10:10:00 -0500 Subject: [PATCH 044/139] Improve logic to balance users across workers so that each worker gets a similar number of users --- locust/dispatch.py | 13 +++ locust/test/test_dispatch.py | 161 +++++++++++++++++++++++++++++++++-- locust/test/test_runners.py | 48 +++++------ 3 files changed, 191 insertions(+), 31 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 6e6180b15a..55800bd9fa 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -230,12 +230,25 @@ def balance_users_among_workers( user_class_occurrences = user_class_occurrences.copy() + total_users = sum(user_class_occurrences.values()) + users_per_worker, remainder = divmod(total_users, len(worker_nodes)) + for user_class in sorted(user_class_occurrences.keys()): if sum(user_class_occurrences.values()) == 0: break for worker_node in itertools.cycle(worker_nodes): if user_class_occurrences[user_class] == 0: break + if ( + sum(balanced_users[worker_node.id].values()) == users_per_worker + and total_users - sum(map(sum, map(lambda x: x.values(), balanced_users.values()))) > remainder + ): + continue + elif ( + sum(balanced_users[worker_node.id].values()) == users_per_worker + 1 + and total_users - sum(map(sum, map(lambda x: x.values(), balanced_users.values()))) < remainder + ): + continue balanced_users[worker_node.id][user_class] += 1 user_class_occurrences[user_class] -= 1 diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 74b147093d..7b1089575f 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -66,9 +66,9 @@ def test_balance_users_among_3_workers(self): user_class_occurrences={"User1": 5, "User2": 4, "User3": 2}, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 2, "User3": 1}, + "1": {"User1": 2, "User2": 1, "User3": 1}, "2": {"User1": 2, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 2, "User3": 0}, } self.assertDictEqual(balanced_users, expected_balanced_users) @@ -77,9 +77,9 @@ def test_balance_users_among_3_workers(self): user_class_occurrences={"User1": 1, "User2": 1, "User3": 1}, ) expected_balanced_users = { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, } self.assertDictEqual(balanced_users, expected_balanced_users) @@ -88,8 +88,8 @@ def test_balance_users_among_3_workers(self): user_class_occurrences={"User1": 1, "User2": 1, "User3": 0}, ) expected_balanced_users = { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, } self.assertDictEqual(balanced_users, expected_balanced_users) @@ -105,6 +105,153 @@ def test_balance_users_among_3_workers(self): } self.assertDictEqual(balanced_users, expected_balanced_users) + def test_balance_5_users_among_10_workers(self): + worker_nodes = [WorkerNode(str(i)) for i in range(1, 11)] + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 10, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, + ) + expected_balanced_users = { + "1": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "6": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 6}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 5, "User3": 5, "User4": 6, "User5": 6}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 5, "User3": 6, "User4": 6, "User5": 6}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "7": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 6}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "6": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "7": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 7}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "6": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "7": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + + balanced_users = balance_users_among_workers( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 11}, + ) + expected_balanced_users = { + "1": {"User1": 2, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 4 users + "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "6": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 2}, # 4 users + "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users + "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users + "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users + } + self.assertDictEqual(balanced_users, expected_balanced_users) + class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index bf21121743..8e711710d8 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -713,11 +713,11 @@ def tick(self): self.assertEqual("spawning", master.state) sleep(5) # runtime = 5s self.assertEqual("running", master.state) - w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} - w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} - w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} - w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} + w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} self.assertDictEqual(w1, workers[0].user_class_occurrences) self.assertDictEqual(w2, workers[1].user_class_occurrences) self.assertDictEqual(w3, workers[2].user_class_occurrences) @@ -734,11 +734,11 @@ def tick(self): self.assertEqual("spawning", master.state) sleep(5) # runtime = 15s self.assertEqual("running", master.state) - w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} - w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 1} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 2} self.assertDictEqual(w1, workers[0].user_class_occurrences) self.assertDictEqual(w2, workers[1].user_class_occurrences) self.assertDictEqual(w3, workers[2].user_class_occurrences) @@ -780,11 +780,11 @@ def tick(self): # TestUser1/TestUser2 have not reached stop timeout yet, so # their number are unchanged self.assertEqual("spawning", master.state) - w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} self.assertDictEqual(w1, workers[0].user_class_occurrences) self.assertDictEqual(w2, workers[1].user_class_occurrences) self.assertDictEqual(w3, workers[2].user_class_occurrences) @@ -800,11 +800,11 @@ def tick(self): # Forth stage - TestUser2/TestUser3 are now at the desired # number, but TestUser1 is still unchanged self.assertEqual("spawning", master.state) - w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w3 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} - w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} - w5 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} + w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 1} + w5 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 1} self.assertDictEqual(w1, workers[0].user_class_occurrences) self.assertDictEqual(w2, workers[1].user_class_occurrences) self.assertDictEqual(w3, workers[2].user_class_occurrences) @@ -819,11 +819,11 @@ def tick(self): # Forth stage - All users are now at the desired number self.assertEqual("running", master.state) - w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} - w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} - w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} - w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} + w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} + w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} + w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} + w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} + w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} self.assertDictEqual(w1, workers[0].user_class_occurrences) self.assertDictEqual(w2, workers[1].user_class_occurrences) self.assertDictEqual(w3, workers[2].user_class_occurrences) From 0b82be4ee9092723cd5b9eab2f7100d9f94666cb Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 10 Feb 2021 15:42:44 -0500 Subject: [PATCH 045/139] Revert on_spawning_complete to previous signature --- locust/event.py | 2 +- locust/runners.py | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/locust/event.py b/locust/event.py index 092e2db403..f79942c764 100644 --- a/locust/event.py +++ b/locust/event.py @@ -119,7 +119,7 @@ class Events: Event arguments: - :param user_class_occurrences: Number of users for each class that were spawned + :param user_count: Number of users that were spawned """ quitting: EventHook diff --git a/locust/runners.py b/locust/runners.py index 92eece7723..6a1be0158b 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -94,7 +94,7 @@ def on_request_failure(request_type, name, response_time, response_length, excep self.connection_broken = False # register listener that resets stats when spawning is complete - def on_spawning_complete(user_class_occurrences): + def on_spawning_complete(user_count): self.update_state(STATE_RUNNING) if environment.reset_stats: logger.info("Resetting stats\n") @@ -306,7 +306,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): self.spawn_users(user_classes_spawn_count, wait) self.stop_users(user_classes_stop_count) - self.environment.events.spawning_complete.fire(user_class_occurrences=self.target_user_class_occurrences) + self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_class_occurrences.values())) def start_shape(self): if self.shape_greenlet: @@ -740,7 +740,7 @@ def client_listener(self): self.clients[msg.node_id].state = STATE_RUNNING self.clients[msg.node_id].user_class_occurrences = msg.data["user_class_occurrences"] if len(self.clients.spawning) == 0: - self.environment.events.spawning_complete.fire(user_class_occurrences=self.user_class_occurrences) + self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_class_occurrences.values())) elif msg.type == "quit": if msg.node_id in self.clients: del self.clients[msg.node_id] @@ -790,9 +790,10 @@ def __init__(self, environment, master_host, master_port): self.greenlet.spawn(self.stats_reporter).link_exception(greenlet_exception_handler) # register listener for when all users have spawned, and report it to the master node - def on_spawning_complete(user_class_occurrences): + def on_spawning_complete(user_count): + assert user_count == sum(self.user_class_occurrences.values()) self.client.send( - Message("spawning_complete", {"user_class_occurrences": user_class_occurrences}, self.client_id) + Message("spawning_complete", {"user_class_occurrences": self.user_class_occurrences}, self.client_id) ) self.worker_state = STATE_RUNNING @@ -848,7 +849,7 @@ def start_worker(self, user_class_occurrences: Dict[str, int], **kwargs): self.spawn_users(user_classes_spawn_count) self.stop_users(user_classes_stop_count) - self.environment.events.spawning_complete.fire(user_class_occurrences=self.user_class_occurrences) + self.environment.events.spawning_complete.fire(user_count=sum(self.user_class_occurrences.values())) def heartbeat(self): while True: From 3232b364a702d2dd4c21e27a54edbdd93e66fa69 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 10 Feb 2021 15:51:01 -0500 Subject: [PATCH 046/139] Run Black on codebase --- locust/runners.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locust/runners.py b/locust/runners.py index 6a1be0158b..44664cd26c 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -740,7 +740,9 @@ def client_listener(self): self.clients[msg.node_id].state = STATE_RUNNING self.clients[msg.node_id].user_class_occurrences = msg.data["user_class_occurrences"] if len(self.clients.spawning) == 0: - self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_class_occurrences.values())) + self.environment.events.spawning_complete.fire( + user_count=sum(self.target_user_class_occurrences.values()) + ) elif msg.type == "quit": if msg.node_id in self.clients: del self.clients[msg.node_id] From 5972e2b944d112c48c4a03564d82b2b2665c403a Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 2 Mar 2021 14:18:00 -0500 Subject: [PATCH 047/139] Remove duplicate imports --- locust/test/test_runners.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 8e711710d8..504f76df34 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -11,7 +11,6 @@ import locust from locust import ( LoadTestShape, - between, constant, runners, ) @@ -25,8 +24,6 @@ from locust.runners import ( LocalRunner, STATE_INIT, - STATE_MISSING, - STATE_RUNNING, STATE_SPAWNING, STATE_RUNNING, STATE_MISSING, From b02fa4881c5ee875ea6f0d0e64be06b69adb873d Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 3 Mar 2021 10:03:13 -0500 Subject: [PATCH 048/139] Adapt changes of ec1d27fa7c523da151f9e9aafcd4a64468adae90 Instead of sending user_count, the worker sends user_class_occurrences --- locust/runners.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 44664cd26c..f53534b866 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -721,9 +721,9 @@ def client_listener(self): logger.info( "Worker %s self-healed with heartbeat, setting state to %s." % (str(c.id), client_state) ) - user_count = msg.data.get("count") - if user_count: - c.user_count = user_count + user_class_occurrences = msg.data.get("user_class_occurrences") + if user_class_occurrences: + c.user_class_occurrences = user_class_occurrences c.state = client_state c.cpu_usage = msg.data["current_cpu_usage"] if not c.cpu_warning_emitted and c.cpu_usage > 90: @@ -862,7 +862,7 @@ def heartbeat(self): { "state": self.worker_state, "current_cpu_usage": self.current_cpu_usage, - "count": self.user_count, + "user_class_occurrences": self.user_class_occurrences, }, self.client_id, ) From 327541f28b0244fe3c0a2cd1f0660d83a0bf4460 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 3 Mar 2021 13:40:02 -0500 Subject: [PATCH 049/139] Increase sleep time to reduce test flakyness --- locust/test/test_runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 85b95b332e..347a2121e2 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -717,7 +717,7 @@ def tick(self): # Start a shape test master.start_shape() - sleep(0.1) + sleep(0.2) # First stage self.assertEqual("spawning", master.state) From 47590f07632b07e64dff498b1e72e475d30fd67a Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 3 Mar 2021 17:41:18 -0500 Subject: [PATCH 050/139] Handle keyboard interrupt while in a gevent.sleep in dispatch_users --- locust/runners.py | 90 +++++++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 39 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index f53534b866..463455c618 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -283,28 +283,34 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): ) self.update_state(STATE_SPAWNING) - for dispatched_users in users_dispatcher: - user_classes_spawn_count = {} - user_classes_stop_count = {} - user_class_occurrences = dispatched_users[dummy_worker_node.id] - for user_class, occurrences in user_class_occurrences.items(): - logger.debug( - "Updating running test with %d users of class %s and wait=%r" % (occurrences, user_class, wait) - ) - if self.user_class_occurrences[user_class] > occurrences: - user_classes_stop_count[user_class] = self.user_class_occurrences[user_class] - occurrences - elif self.user_class_occurrences[user_class] < occurrences: - user_classes_spawn_count[user_class] = occurrences - self.user_class_occurrences[user_class] - - if wait: - # spawn_users will block, so we need to call stop_users first - self.stop_users(user_classes_stop_count) - self.spawn_users(user_classes_spawn_count, wait) - else: - # call spawn_users before stopping the users since stop_users - # can be blocking because of the stop_timeout - self.spawn_users(user_classes_spawn_count, wait) - self.stop_users(user_classes_stop_count) + try: + for dispatched_users in users_dispatcher: + user_classes_spawn_count = {} + user_classes_stop_count = {} + user_class_occurrences = dispatched_users[dummy_worker_node.id] + for user_class, occurrences in user_class_occurrences.items(): + logger.debug( + "Updating running test with %d users of class %s and wait=%r" % (occurrences, user_class, wait) + ) + if self.user_class_occurrences[user_class] > occurrences: + user_classes_stop_count[user_class] = self.user_class_occurrences[user_class] - occurrences + elif self.user_class_occurrences[user_class] < occurrences: + user_classes_spawn_count[user_class] = occurrences - self.user_class_occurrences[user_class] + + if wait: + # spawn_users will block, so we need to call stop_users first + self.stop_users(user_classes_stop_count) + self.spawn_users(user_classes_spawn_count, wait) + else: + # call spawn_users before stopping the users since stop_users + # can be blocking because of the stop_timeout + self.spawn_users(user_classes_spawn_count, wait) + self.stop_users(user_classes_stop_count) + + except KeyboardInterrupt: + # We need to catch keyboard interrupt. Otherwise, if KeyboardInterrupt is received while in + # a gevent.sleep inside the dispatch_users function, locust won't gracefully shutdown. + self.quit() self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_class_occurrences.values())) @@ -595,24 +601,30 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: spawn_rate=spawn_rate, ) - for dispatched_users in users_dispatcher: - dispatch_greenlets = Group() - for worker_node_id, worker_user_class_occurrences in dispatched_users.items(): - data = { - "timestamp": time.time(), - "user_class_occurrences": worker_user_class_occurrences, - "host": self.environment.host, - "stop_timeout": self.environment.stop_timeout, - } - dispatch_greenlets.add( - gevent.spawn_later( - 0, - self.server.send_to_client, - Message("spawn", data, worker_node_id), + try: + for dispatched_users in users_dispatcher: + dispatch_greenlets = Group() + for worker_node_id, worker_user_class_occurrences in dispatched_users.items(): + data = { + "timestamp": time.time(), + "user_class_occurrences": worker_user_class_occurrences, + "host": self.environment.host, + "stop_timeout": self.environment.stop_timeout, + } + dispatch_greenlets.add( + gevent.spawn_later( + 0, + self.server.send_to_client, + Message("spawn", data, worker_node_id), + ) ) - ) - logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) - dispatch_greenlets.join() + logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) + dispatch_greenlets.join() + + except KeyboardInterrupt: + # We need to catch keyboard interrupt. Otherwise, if KeyboardInterrupt is received while in + # a gevent.sleep inside the dispatch_users function, locust won't gracefully shutdown. + self.quit() def stop(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: From 34e633813555d5398bba29cd4f5ca6ab44229796 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 3 Mar 2021 19:49:04 -0500 Subject: [PATCH 051/139] Increase sleep time to reduce test flakyness --- locust/test/test_runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 347a2121e2..835de645d8 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -717,7 +717,7 @@ def tick(self): # Start a shape test master.start_shape() - sleep(0.2) + sleep(0.5) # First stage self.assertEqual("spawning", master.state) From 116fbdca6fb884a03ffe8f0d94421848e8d073ae Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 4 Mar 2021 13:02:43 -0500 Subject: [PATCH 052/139] Refactor test to hopefully reduce flakyness The test is test_distributed_shape_with_stop_timeout --- locust/test/test_runners.py | 45 ++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 835de645d8..9143383b3c 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -717,11 +717,13 @@ def tick(self): # Start a shape test master.start_shape() - sleep(0.5) # First stage - self.assertEqual("spawning", master.state) - sleep(5) # runtime = 5s + ts = time.time() + while master.state != "spawning": + self.assertTrue(time.time() - ts <= 1) + sleep() + sleep(5 - (time.time() - ts)) # runtime = 5s self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} @@ -741,8 +743,11 @@ def tick(self): sleep(5) # runtime = 10s # Second stage - self.assertEqual("spawning", master.state) - sleep(5) # runtime = 15s + ts = time.time() + while master.state != "spawning": + self.assertTrue(time.time() - ts <= 1) + sleep() + sleep(5 - (time.time() - ts)) # runtime = 15s self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} @@ -762,8 +767,11 @@ def tick(self): sleep(5) # runtime = 20s # Third stage - self.assertEqual("spawning", master.state) - sleep(5) # runtime = 25s + ts = time.time() + while master.state != "spawning": + self.assertTrue(time.time() - ts <= 1) + sleep() + sleep(5 - (time.time() - ts)) # runtime = 25s self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} @@ -783,8 +791,11 @@ def tick(self): sleep(5) # runtime = 30s # Forth stage - self.assertEqual("spawning", master.state) - sleep(5) # runtime = 35s + ts = time.time() + while master.state != "spawning": + self.assertTrue(time.time() - ts <= 1) + sleep() + sleep(5 - (time.time() - ts)) # runtime = 35s # Forth stage - Excess TestUser3 have been stopped but # TestUser1/TestUser2 have not reached stop timeout yet, so @@ -809,7 +820,11 @@ def tick(self): # Forth stage - TestUser2/TestUser3 are now at the desired # number, but TestUser1 is still unchanged - self.assertEqual("spawning", master.state) + ts = time.time() + while master.state != "spawning": + self.assertTrue(time.time() - ts <= 1) + sleep() + delta = time.time() - ts w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} @@ -825,10 +840,14 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(5) # runtime = 50s + sleep(5 - delta) # runtime = 50s # Forth stage - All users are now at the desired number - self.assertEqual("running", master.state) + ts = time.time() + while master.state != "running": + self.assertTrue(time.time() - ts <= 1) + sleep() + delta = time.time() - ts w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} @@ -844,7 +863,7 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(10) # runtime = 60s + sleep(10 - delta) # runtime = 60s # Sleep stop_timeout and make sure the test has stopped sleep(1) # runtime = 61s From ea52674aab99e8a4b476456508e9012d4b96d250 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 7 Mar 2021 11:15:57 -0500 Subject: [PATCH 053/139] Add TODO to handle KeyboardInterrupt when waiting for workers connect --- locust/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/locust/main.py b/locust/main.py index 6521ab1051..603545a187 100644 --- a/locust/main.py +++ b/locust/main.py @@ -316,6 +316,9 @@ def main(): len(runner.clients.ready), options.expect_workers, ) + # TODO: Handle KeyboardInterrupt and send quit signal to workers that are started. + # Right now, if the user sends a ctrl+c, the master will not gracefully + # shutdown resulting in all the already started workers to stay active. time.sleep(1) if not options.worker: # apply headless mode defaults From 2a28a7ec14237be1719b71bb57006039eef81a22 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 7 Mar 2021 11:16:36 -0500 Subject: [PATCH 054/139] Make sure stats printer run when using headless mode --- locust/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locust/main.py b/locust/main.py index 603545a187..2aff5689bb 100644 --- a/locust/main.py +++ b/locust/main.py @@ -306,6 +306,7 @@ def main(): web_ui.start() main_greenlet = web_ui.greenlet + headless_master_greenlet = None if options.headless: # headless mode if options.master: @@ -331,7 +332,8 @@ def main(): if environment.shape_class: environment.runner.start_shape() else: - runner.start(options.num_users, options.spawn_rate) + headless_master_greenlet = gevent.spawn(runner.start, options.num_users, options.spawn_rate) + headless_master_greenlet.link_exception(greenlet_exception_handler) def spawn_run_time_limit_greenlet(): def timelimit_stop(): @@ -406,6 +408,8 @@ def shutdown(): logger.info("Shutting down (exit code %s), bye." % code) if stats_printer_greenlet is not None: stats_printer_greenlet.kill(block=False) + if headless_master_greenlet is not None: + headless_master_greenlet.kill(block=False) logger.info("Cleaning up runner...") if runner is not None: runner.quit() From 4d68f14e7b778468a58c027c331943d55b820147 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 7 Mar 2021 11:19:58 -0500 Subject: [PATCH 055/139] Add log indicating that spawning is complete after a ramp up --- locust/runners.py | 38 +++++++++++++++++++++++++++++++++++++ locust/test/test_runners.py | 4 ++++ 2 files changed, 42 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 463455c618..37bfd32aa7 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -5,6 +5,7 @@ import sys import time import traceback +from collections import defaultdict from collections.abc import MutableMapping from typing import ( Dict, @@ -312,6 +313,14 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): # a gevent.sleep inside the dispatch_users function, locust won't gracefully shutdown. self.quit() + logger.info( + "All users spawned: %s (%i total running)" + % ( + ", ".join("%s: %d" % (name, count) for name, count in self.user_class_occurrences.items()), + sum(self.user_class_occurrences.values()), + ) + ) + self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_class_occurrences.values())) def start_shape(self): @@ -626,6 +635,27 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: # a gevent.sleep inside the dispatch_users function, locust won't gracefully shutdown. self.quit() + # Wait a little for workers to report their users to the master + # so that we can give an accurate log message below, i.e. "All users spawned [...]". + # Otherwise, the logged user count might be less than the target user count. + timeout = gevent.Timeout(0.1) + timeout.start() + try: + while self.user_count != self.target_user_count: + gevent.sleep() + except gevent.Timeout: + pass + finally: + timeout.cancel() + + logger.info( + "All users spawned: %s (%i total running)" + % ( + ", ".join("%s: %d" % (name, count) for name, count in self.reported_user_class_occurrences.items()), + sum(self.reported_user_class_occurrences.values()), + ) + ) + def stop(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: logger.debug("Stopping...") @@ -775,6 +805,14 @@ def client_listener(self): def worker_count(self): return len(self.clients.ready) + len(self.clients.spawning) + len(self.clients.running) + @property + def reported_user_class_occurrences(self) -> Dict[str, int]: + reported_user_class_occurrences = defaultdict(lambda: 0) + for client in self.clients.ready + self.clients.spawning + self.clients.running: + for name, count in client.user_class_occurrences.items(): + reported_user_class_occurrences[name] += count + return reported_user_class_occurrences + class WorkerRunner(DistributedRunner): """ diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 9143383b3c..22dddb2bfc 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -617,6 +617,7 @@ def tick(self): self.assertEqual( 9, test_shape.get_current_user_count(), "Shape is not seeing stage 1 runner user count correctly" ) + self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 9}) # Ensure new stage with more users has been reached sleep(2) @@ -625,6 +626,7 @@ def tick(self): self.assertEqual( 21, test_shape.get_current_user_count(), "Shape is not seeing stage 2 runner user count correctly" ) + self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 21}) # Ensure new stage with less users has been reached sleep(2) @@ -633,6 +635,7 @@ def tick(self): self.assertEqual( 3, test_shape.get_current_user_count(), "Shape is not seeing stage 3 runner user count correctly" ) + self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 3}) # Ensure test stops at the end sleep(2) @@ -641,6 +644,7 @@ def tick(self): self.assertEqual( 0, test_shape.get_current_user_count(), "Shape is not seeing stopped runner user count correctly" ) + self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 0}) self.assertEqual("stopped", master.state) From 61399bd7cadc30551c0db996f7d887cc75fb5bea Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 7 Mar 2021 11:59:08 -0500 Subject: [PATCH 056/139] Rework logging to be less spammy when level is INFO --- locust/runners.py | 25 +++++++++++++++++-------- locust/test/test_main.py | 15 ++++++++++++++- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 37bfd32aa7..e103403b70 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -164,10 +164,16 @@ def spawn_users(self, user_classes_spawn_count: Dict[str, int], wait: bool = Fal if self.state == STATE_INIT or self.state == STATE_STOPPED: self.update_state(STATE_SPAWNING) - logger.info( - "Spawning additional %s (%s already running)..." - % (json.dumps(user_classes_spawn_count), json.dumps(self.user_class_occurrences)) - ) + if logger.isEnabledFor(logging.DEBUG): + logger.debug( + "Spawning additional %s (%s already running)..." + % (json.dumps(user_classes_spawn_count), json.dumps(self.user_class_occurrences)) + ) + elif sum(user_classes_spawn_count.values()) > 0: + logger.info( + "Spawning additional %s (%s already running)..." + % (sum(user_classes_spawn_count.values()), sum(self.user_class_occurrences.values())) + ) def spawn(user_class: str, spawn_count: int): n = 0 @@ -177,7 +183,7 @@ def spawn(user_class: str, spawn_count: int): n += 1 if n % 10 == 0 or n == spawn_count: logger.debug("%i users spawned" % self.user_count) - logger.info("All users of class %s spawned" % user_class) + logger.debug("All users of class %s spawned" % user_class) for user_class, spawn_count in user_classes_spawn_count.items(): spawn(user_class, spawn_count) @@ -228,9 +234,11 @@ def stop_users(self, user_classes_stop_count: Dict[str, int]): ) stop_group.kill(block=True) - logger.info( - "%i Users have been stopped, %g still running" % (sum(user_classes_stop_count.values()), self.user_count) - ) + msg = "%i Users have been stopped, %g still running" % (sum(user_classes_stop_count.values()), self.user_count) + if logger.isEnabledFor(logging.DEBUG): + logger.debug(msg) + elif sum(user_classes_stop_count.values()) > 0: + logger.info(msg) def monitor_cpu(self): process = psutil.Process() @@ -289,6 +297,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): user_classes_spawn_count = {} user_classes_stop_count = {} user_class_occurrences = dispatched_users[dummy_worker_node.id] + logger.info("Updating running test with %d users" % (sum(user_class_occurrences.values()),)) for user_class, occurrences in user_class_occurrences.items(): logger.debug( "Updating running test with %d users of class %s and wait=%r" % (occurrences, user_class, wait) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index ac7a57491b..e4b3860b36 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -229,7 +229,18 @@ def test_default_headless_spawn_options(self): with mock_locustfile() as mocked: output = ( subprocess.check_output( - ["locust", "-f", mocked.file_path, "--host", "https://test.com/", "--run-time", "1s", "--headless"], + [ + "locust", + "-f", + mocked.file_path, + "--host", + "https://test.com/", + "--run-time", + "1s", + "--headless", + "--loglevel", + "DEBUG", + ], stderr=subprocess.STDOUT, timeout=2, ) @@ -341,6 +352,8 @@ def t(self): "7s", "-u", "0", + "--loglevel", + "DEBUG", ] ), stderr=STDOUT, From 6bb4b4158b7f4e4c3fc8e5c2b2583bee582cb1d6 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 15 Mar 2021 13:07:31 -0400 Subject: [PATCH 057/139] Ensure user class are balanced during ramp-up --- locust/dispatch.py | 137 ++++++++------ locust/test/test_dispatch.py | 339 ++++++++++++++++++----------------- locust/test/test_runners.py | 6 +- 3 files changed, 260 insertions(+), 222 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 55800bd9fa..c6b10a7522 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -7,7 +7,6 @@ Generator, List, TYPE_CHECKING, - Tuple, ) import gevent @@ -92,20 +91,36 @@ def dispatch_users( if less_users_than_desired: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: + ts1 = time.time() number_of_users_in_current_dispatch = 0 - for user_class in user_class_occurrences.keys(): - if all(x[user_class] == 0 for x in effective_balanced_users.values()): + for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): + assert i < 5000, i + + if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: + break + + if all_users_of_current_class_have_been_dispatched(effective_balanced_users, user_class): + continue + + if go_to_next_user_class( + user_class, user_class_occurrences, dispatched_users, effective_balanced_users + ): continue - done, number_of_users_in_current_dispatch = distribute_current_user_class_among_workers( - dispatched_users, - effective_balanced_users, - user_class, - number_of_users_in_current_dispatch, - number_of_users_per_dispatch, - ) - if done: + + for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): + assert j < 100, j + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 + break + + if number_of_users_in_current_dispatch == number_of_users_per_dispatch: break + assert time.time() - ts1 < 0.5, time.time() - ts1 + ts = time.time() yield { worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) @@ -113,7 +128,9 @@ def dispatch_users( } if sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: delta = time.time() - ts - gevent.sleep(max(0.0, wait_between_dispatch - delta)) + sleep_duration = max(0.0, wait_between_dispatch - delta) + assert sleep_duration <= 10, sleep_duration + gevent.sleep(sleep_duration) elif ( number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) @@ -123,29 +140,45 @@ def dispatch_users( else: while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): + ts1 = time.time() number_of_users_in_current_dispatch = 0 - for user_class in user_class_occurrences.keys(): - if all_users_of_current_class_have_been_dispatched( - dispatched_users, effective_balanced_users, user_class + for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): + assert i < 5000, i + + if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: + break + + if all_users_of_current_class_have_been_dispatched(effective_balanced_users, user_class): + continue + + if go_to_next_user_class( + user_class, user_class_occurrences, dispatched_users, effective_balanced_users ): continue - done, number_of_users_in_current_dispatch = distribute_current_user_class_among_workers( - dispatched_users, - effective_balanced_users, - user_class, - number_of_users_in_current_dispatch, - number_of_users_per_dispatch, - ) - if done: + + for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): + assert j < 100, j + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 break + if number_of_users_in_current_dispatch == number_of_users_per_dispatch: + break + + assert time.time() - ts1 < 0.5, time.time() - ts1 + ts = time.time() yield { worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) } delta = time.time() - ts - gevent.sleep(max(0.0, wait_between_dispatch - delta)) + sleep_duration = max(0.0, wait_between_dispatch - delta) + assert sleep_duration <= 10, sleep_duration + gevent.sleep(sleep_duration) # If we are here, it means we have an excess of users for one or more user classes. # Hence, we need to dispatch a last set of users that will bring the users @@ -153,6 +186,31 @@ def dispatch_users( yield balanced_users +def go_to_next_user_class( + user_class: str, + user_class_occurrences: Dict[str, int], + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], +) -> bool: + """ + Whether to skip to next user class or not. This is done so that + the distribution of user class stays approximately balanced during + a ramp up. + """ + dispatched_user_class_occurrences = { + user_class_: sum(x[user_class_] for x in dispatched_users.values()) + for user_class_ in user_class_occurrences.keys() + } + for user_class_ in sorted(user_class_occurrences.keys()): + if user_class_ == user_class: + continue + if sum(x[user_class_] for x in effective_balanced_users.values()) == 0: + continue + if dispatched_user_class_occurrences[user_class] - dispatched_user_class_occurrences[user_class_] >= 1: + return True + return False + + def number_of_users_left_to_dispatch( dispatched_users: Dict[str, Dict[str, int]], balanced_users: Dict[str, Dict[str, int]], @@ -167,32 +225,6 @@ def number_of_users_left_to_dispatch( ) -def distribute_current_user_class_among_workers( - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], - user_class: str, - number_of_users_in_current_dispatch: int, - number_of_users_per_dispatch: int, -) -> Tuple[bool, int]: - """ - :return done: boolean indicating if we have enough users to perform a dispatch to the workers - :return number_of_users_in_current_dispatch: current number of users in the dispatch - """ - done = False - for worker_node_id in itertools.cycle(effective_balanced_users.keys()): - if effective_balanced_users[worker_node_id][user_class] == 0: - continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 - number_of_users_in_current_dispatch += 1 - if number_of_users_in_current_dispatch == number_of_users_per_dispatch: - done = True - break - if all(x[user_class] == 0 for x in effective_balanced_users.values()): - break - return done, number_of_users_in_current_dispatch - - def all_users_have_been_dispatched( dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], @@ -206,13 +238,10 @@ def all_users_have_been_dispatched( def all_users_of_current_class_have_been_dispatched( - dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], user_class: str, ) -> bool: - return sum(x[user_class] for x in dispatched_users.values()) >= sum( - x[user_class] for x in effective_balanced_users.values() - ) + return all(x[user_class] == 0 for x in effective_balanced_users.values()) def balance_users_among_workers( diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 7b1089575f..f67d7767e2 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -1,3 +1,4 @@ +import random import time import unittest @@ -283,8 +284,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -295,9 +296,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -307,9 +308,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -319,9 +320,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -331,9 +332,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -344,8 +345,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -407,8 +408,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -419,9 +420,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -431,9 +432,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -443,9 +444,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -455,9 +456,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -468,8 +469,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -531,8 +532,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -543,9 +544,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -555,9 +556,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -567,9 +568,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -579,9 +580,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -592,8 +593,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -643,8 +644,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -655,9 +656,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -667,9 +668,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -721,8 +722,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -733,9 +734,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -745,9 +746,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -797,9 +798,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -809,9 +810,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -849,9 +850,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) delta = time.time() - ts @@ -936,7 +937,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 1, "User3": 0}, }, @@ -948,9 +949,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -960,9 +961,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -972,9 +973,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -985,7 +986,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 1}, "3": {"User1": 1, "User2": 1, "User3": 0}, }, ) @@ -1039,7 +1040,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 1, "User3": 0}, }, @@ -1051,9 +1052,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1063,9 +1064,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1075,9 +1076,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1088,7 +1089,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 1}, "3": {"User1": 1, "User2": 1, "User3": 0}, }, ) @@ -1142,7 +1143,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 1, "User3": 0}, }, @@ -1154,9 +1155,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1166,9 +1167,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1178,9 +1179,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1191,7 +1192,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 0, "User3": 1}, "3": {"User1": 1, "User2": 1, "User3": 0}, }, ) @@ -1245,9 +1246,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1257,9 +1258,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1314,9 +1315,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1326,9 +1327,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1381,9 +1382,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1436,9 +1437,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) delta = time.time() - ts @@ -1911,6 +1912,45 @@ def test_dispatch_users_to_3_workers(self): self.assertTrue(0 <= delta <= 0.02, delta) +class TestDispatchUsersToWorkersFuzzy(unittest.TestCase): + def test_dispatch_users_to_workers(self): + """ + This "fuzzy" test uses the "dispatch_users" with various random + input parameters to validate that the dispatch logic does not get stuck + in some infinite loop or other unforeseen situations. + """ + + for _ in range(20): + for max_prior_users in [0, 3, 5]: + number_of_user_classes = random.randint(1, 30) + number_of_workers = random.randint(1, 30) + + worker_nodes = [] + for i in range(1, number_of_workers + 1): + worker_node = WorkerNode(str(i)) + worker_node.user_class_occurrences = { + f"User{i}": random.randint(0, max_prior_users) + for i in range(1, random.randint(1, number_of_user_classes + 1)) + } + worker_nodes.append(worker_node) + + user_class_occurrences = { + f"User{i}": random.randint(0, 50) for i in range(1, number_of_user_classes + 1) + } + total_number_of_users = sum(user_class_occurrences.values()) + # We limit the maximum total dispatch to around 10s (i.e. total_number_of_users / 10) + # so that the test does take too much time. + spawn_rate = max(total_number_of_users / 10, 100 * random.random()) + + users_dispatcher = dispatch_users( + worker_nodes=worker_nodes, + user_class_occurrences=user_class_occurrences, + spawn_rate=spawn_rate, + ) + + dispatched = list(users_dispatcher) + + class TestNumberOfUsersLeftToDispatch(unittest.TestCase): def test_number_of_users_left_to_dispatch(self): user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} @@ -2000,76 +2040,41 @@ def test_all_users_have_been_dispatched(self): class TestAllUsersOfCurrentClassHaveBeenDispatched(unittest.TestCase): def test_all_users_of_current_class_have_been_dispatched(self): effective_balanced_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + "Worker1": {"User1": 0, "User2": 0, "User3": 0}, + "Worker2": {"User1": 0, "User2": 0, "User3": 0}, } + self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - dispatched_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + effective_balanced_users = { + "Worker1": {"User1": 1, "User2": 0, "User3": 1}, + "Worker2": {"User1": 0, "User2": 1, "User3": 0}, } - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") - ) - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") - ) - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") - ) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - dispatched_users = { - "Worker1": {"User1": 4, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") - ) - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") - ) - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") - ) - - dispatched_users = { - "Worker1": {"User1": 2, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, + effective_balanced_users = { + "Worker1": {"User1": 0, "User2": 1, "User3": 4}, + "Worker2": {"User1": 0, "User2": 1, "User3": 4}, } - self.assertFalse( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") - ) - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") - ) - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") - ) + self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - dispatched_users = { - "Worker1": {"User1": 0, "User2": 0, "User3": 0}, + effective_balanced_users = { + "Worker1": {"User1": 1, "User2": 1, "User3": 1}, "Worker2": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertFalse( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") - ) - self.assertFalse( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") - ) - self.assertFalse( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") - ) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - dispatched_users = { + effective_balanced_users = { "Worker1": {"User1": 4, "User2": 0, "User3": 0}, "Worker2": {"User1": 4, "User2": 0, "User3": 0}, } - self.assertTrue( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User1") - ) - self.assertFalse( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User2") - ) - self.assertFalse( - all_users_of_current_class_have_been_dispatched(dispatched_users, effective_balanced_users, "User3") - ) + self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) + self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 22dddb2bfc..f63cb4eabb 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -776,6 +776,10 @@ def tick(self): self.assertTrue(time.time() - ts <= 1) sleep() sleep(5 - (time.time() - ts)) # runtime = 25s + ts = time.time() + while master.state != "running": + self.assertTrue(time.time() - ts <= 1) + sleep() self.assertEqual("running", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} @@ -792,7 +796,7 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(5) # runtime = 30s + sleep(5 - (time.time() - ts)) # runtime = 30s # Forth stage ts = time.time() From 8afbd15bef24f304cea79c011627626ef8db8025 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 15 Mar 2021 13:31:27 -0400 Subject: [PATCH 058/139] Relocate duplicate code block in function --- locust/dispatch.py | 119 ++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 66 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index c6b10a7522..b43cfd82a6 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -1,6 +1,7 @@ import itertools import math import time +from collections import namedtuple from copy import deepcopy from typing import ( Dict, @@ -91,41 +92,12 @@ def dispatch_users( if less_users_than_desired: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - ts1 = time.time() - number_of_users_in_current_dispatch = 0 - for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): - assert i < 5000, i - - if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: - break - - if all_users_of_current_class_have_been_dispatched(effective_balanced_users, user_class): - continue - - if go_to_next_user_class( - user_class, user_class_occurrences, dispatched_users, effective_balanced_users - ): - continue - - for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): - assert j < 100, j - if effective_balanced_users[worker_node_id][user_class] == 0: - continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 - number_of_users_in_current_dispatch += 1 - break - - if number_of_users_in_current_dispatch == number_of_users_per_dispatch: - break - - assert time.time() - ts1 < 0.5, time.time() - ts1 + users_to_dispatch = get_users_to_dispatch_for_current_iteration( + user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch + ) ts = time.time() - yield { - worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) - for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) - } + yield users_to_dispatch if sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: delta = time.time() - ts sleep_duration = max(0.0, wait_between_dispatch - delta) @@ -140,41 +112,12 @@ def dispatch_users( else: while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): - ts1 = time.time() - number_of_users_in_current_dispatch = 0 - for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): - assert i < 5000, i - - if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: - break - - if all_users_of_current_class_have_been_dispatched(effective_balanced_users, user_class): - continue - - if go_to_next_user_class( - user_class, user_class_occurrences, dispatched_users, effective_balanced_users - ): - continue - - for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): - assert j < 100, j - if effective_balanced_users[worker_node_id][user_class] == 0: - continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 - number_of_users_in_current_dispatch += 1 - break - - if number_of_users_in_current_dispatch == number_of_users_per_dispatch: - break - - assert time.time() - ts1 < 0.5, time.time() - ts1 + users_to_dispatch = get_users_to_dispatch_for_current_iteration( + user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch + ) ts = time.time() - yield { - worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) - for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) - } + yield users_to_dispatch delta = time.time() - ts sleep_duration = max(0.0, wait_between_dispatch - delta) assert sleep_duration <= 10, sleep_duration @@ -186,6 +129,50 @@ def dispatch_users( yield balanced_users +def get_users_to_dispatch_for_current_iteration( + user_class_occurrences: Dict[str, int], + dispatched_users: Dict[str, Dict[str, int]], + effective_balanced_users: Dict[str, Dict[str, int]], + number_of_users_per_dispatch: int, +) -> Dict[str, Dict[str, int]]: + ts_dispatch = time.time() + + number_of_users_in_current_dispatch = 0 + + for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): + assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) + + if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: + break + + if all_users_of_current_class_have_been_dispatched(effective_balanced_users, user_class): + continue + + if go_to_next_user_class(user_class, user_class_occurrences, dispatched_users, effective_balanced_users): + continue + + for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): + assert j < 100, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 + break + + if number_of_users_in_current_dispatch == number_of_users_per_dispatch: + break + + assert time.time() - ts_dispatch < 0.5, "Dispatch iteration took too much time: {}s".format( + time.time() - ts_dispatch + ) + + return { + worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) + for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) + } + + def go_to_next_user_class( user_class: str, user_class_occurrences: Dict[str, int], From 2833b88addceda858bad91eb3180de8e98502204 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 16 Mar 2021 12:27:14 -0400 Subject: [PATCH 059/139] Handle user_greenlet.args[0] IndexError Similar to 0d58646e88708c6ca5b5d0f562f221001c385d7f --- locust/runners.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locust/runners.py b/locust/runners.py index b701c74c53..b90f01517d 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -138,7 +138,14 @@ def user_class_occurrences(self) -> Dict[str, int]: """ user_class_occurrences = {user_class.__name__: 0 for user_class in self.user_classes} for user_greenlet in self.user_greenlets: - user = user_greenlet.args[0] + try: + user = user_greenlet.args[0] + except IndexError: + logger.error( + "While calculating number of running users, we encountered a user that didnt have proper args %s", + user_greenlet, + ) + continue user_class_occurrences[user.__class__.__name__] += 1 return user_class_occurrences From abd183a39f244809e06e54f802e68604d8e33059 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 4 May 2021 15:12:10 -0400 Subject: [PATCH 060/139] Fix typo in comments --- locust/test/test_runners.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 594f76de95..ba779c0d35 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -804,16 +804,16 @@ def tick(self): self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) sleep(5 - (time.time() - ts)) # runtime = 30s - # Forth stage + # Fourth stage ts = time.time() while master.state != "spawning": self.assertTrue(time.time() - ts <= 1) sleep() sleep(5 - (time.time() - ts)) # runtime = 35s - # Forth stage - Excess TestUser3 have been stopped but - # TestUser1/TestUser2 have not reached stop timeout yet, so - # their number are unchanged + # Fourth stage - Excess TestUser3 have been stopped but + # TestUser1/TestUser2 have not reached stop timeout yet, so + # their number are unchanged self.assertEqual("spawning", master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} @@ -832,8 +832,8 @@ def tick(self): self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) sleep(10) # runtime = 45s - # Forth stage - TestUser2/TestUser3 are now at the desired - # number, but TestUser1 is still unchanged + # Fourth stage - TestUser2/TestUser3 are now at the desired + # number, but TestUser1 is still unchanged ts = time.time() while master.state != "spawning": self.assertTrue(time.time() - ts <= 1) @@ -856,7 +856,7 @@ def tick(self): self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) sleep(5 - delta) # runtime = 50s - # Forth stage - All users are now at the desired number + # Fourth stage - All users are now at the desired number ts = time.time() while master.state != "running": self.assertTrue(time.time() - ts <= 1) From 162e87afe7c85354153cec014f96b9232f718716 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 4 May 2021 15:19:00 -0400 Subject: [PATCH 061/139] Also include "user_count" in message payload This ensures that plugins or user's code won't break if they used this value before. --- locust/runners.py | 7 ++++- locust/test/test_runners.py | 58 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/locust/runners.py b/locust/runners.py index 8109be7b76..fa3ed7575f 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -868,7 +868,11 @@ def __init__(self, environment, master_host, master_port): def on_spawning_complete(user_count): assert user_count == sum(self.user_class_occurrences.values()) self.client.send( - Message("spawning_complete", {"user_class_occurrences": self.user_class_occurrences}, self.client_id) + Message( + "spawning_complete", + {"user_class_occurrences": self.user_class_occurrences, "user_count": self.user_count}, + self.client_id, + ) ) self.worker_state = STATE_RUNNING @@ -877,6 +881,7 @@ def on_spawning_complete(user_count): # register listener that adds the current number of spawned users to the report that is sent to the master node def on_report_to_master(client_id, data): data["user_class_occurrences"] = self.user_class_occurrences + data["user_count"] = self.user_count self.environment.events.report_to_master.add_listener(on_report_to_master) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index ba779c0d35..9284170439 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1863,6 +1863,64 @@ def my_task(self): worker.quit() + def test_worker_messages_sent_to_master(self): + """ + Ensure that worker includes both "user_count" and "user_class_occurrences" + when reporting to the master. + """ + + class MyUser(User): + wait_time = constant(1) + + def start(self, group: Group): + # We do this so that the spawning does not finish + # too quickly + gevent.sleep(0.1) + return super().start(group) + + @task + def my_task(self): + pass + + with mock.patch("locust.rpc.rpc.Client", mocked_rpc()) as client: + environment = Environment() + worker = self.get_runner(environment=environment, user_classes=[MyUser]) + + client.mocked_send( + Message( + "spawn", + { + "timestamp": 1605538584, + "user_class_occurrences": {"MyUser": 10}, + "host": "", + "stop_timeout": None, + }, + "dummy_client_id", + ) + ) + sleep(0.6) + self.assertEqual(STATE_SPAWNING, worker.state) + worker.spawning_greenlet.join() + self.assertEqual(10, worker.user_count) + + sleep(2) + + message = next((m for m in reversed(client.outbox) if m.type == "stats"), None) + self.assertIsNotNone(message) + self.assertIn("user_count", message.data) + self.assertIn("user_class_occurrences", message.data) + self.assertEqual(message.data["user_count"], 10) + self.assertEqual(message.data["user_class_occurrences"]["MyUser"], 10) + + message = next((m for m in client.outbox if m.type == "spawning_complete"), None) + self.assertIsNotNone(message) + self.assertIn("user_count", message.data) + self.assertIn("user_class_occurrences", message.data) + self.assertEqual(message.data["user_count"], 10) + self.assertEqual(message.data["user_class_occurrences"]["MyUser"], 10) + + worker.quit() + def test_change_user_count_during_spawning(self): class MyUser(User): wait_time = constant(1) From 45dbffc483a74b1c172d9f0d85ff7cddd7ccdfde Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 31 May 2021 12:52:03 -0400 Subject: [PATCH 062/139] Fix performance issue The perf issue was discovered in https://github.com/locustio/locust/pull/1621#issuecomment-851312623 --- locust/distribution.py | 47 +++++++++++++++++++++---------- locust/test/test_distribution.py | 48 +++++++++++++++++--------------- 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 1f7403dcd6..92101f2bee 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -76,22 +76,39 @@ def _find_ideal_users_to_add_or_remove( number_of_users_to_add_or_remove = abs(number_of_users_to_add_or_remove) - user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} - - for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add_or_remove): - user_class_occurrences_candidate = { - user_class.__name__: user_class_occurrences[user_class.__name__] - + sign * sum(1 for user_class_ in user_classes_combination if user_class_.__name__ == user_class.__name__) - for user_class in user_classes - } - distance = distance_from_desired_distribution( - user_classes, - user_class_occurrences_candidate, - ) - if distance not in user_class_occurrences_candidates: - user_class_occurrences_candidates[distance] = user_class_occurrences_candidate.copy() + assert number_of_users_to_add_or_remove <= len(user_classes), number_of_users_to_add_or_remove + + # Formula for combination with replacement + # (https://www.tutorialspoint.com/statistics/combination_with_replacement.htm) + number_of_combinations = math.factorial(len(user_classes) + number_of_users_to_add_or_remove - 1) / ( + math.factorial(number_of_users_to_add_or_remove) * math.factorial(len(user_classes) - 1) + ) + + # If the number of combinations with replacement is above this threshold, we simply add/remove + # users for the first "number_of_users_to_add_or_remove" users. Otherwise, computing the best + # distribution is too expensive in terms of computation. + max_number_of_combinations_threshold = 1000 + + if number_of_combinations <= max_number_of_combinations_threshold: + user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} + for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add_or_remove): + user_class_occurrences_candidate = user_class_occurrences.copy() + for user_class in user_classes_combination: + user_class_occurrences_candidate[user_class.__name__] += sign + distance = distance_from_desired_distribution( + user_classes, + user_class_occurrences_candidate, + ) + if distance not in user_class_occurrences_candidates: + user_class_occurrences_candidates[distance] = user_class_occurrences_candidate.copy() + + return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] - return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] + else: + user_class_occurrences_candidate = user_class_occurrences.copy() + for user_class in user_classes[:number_of_users_to_add_or_remove]: + user_class_occurrences_candidate[user_class.__name__] += sign + return user_class_occurrences_candidate def distance_from_desired_distribution( diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index c34467c0d2..4c77229367 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -1,3 +1,4 @@ +import time import unittest from locust import User @@ -274,25 +275,28 @@ class User14(User): class User15(User): weight = 69 - number_of_users = 1044523783783 - user_class_occurrences = weight_users( - user_classes=[ - User1, - User2, - User3, - User4, - User5, - User6, - User7, - User8, - User9, - User10, - User11, - User12, - User13, - User14, - User15, - ], - number_of_users=number_of_users, - ) - self.assertEqual(sum(user_class_occurrences.values()), number_of_users) + for number_of_users in range(1044523783783, 1044523783783 + 1000): + ts = time.perf_counter_ns() + user_class_occurrences = weight_users( + user_classes=[ + User1, + User2, + User3, + User4, + User5, + User6, + User7, + User8, + User9, + User10, + User11, + User12, + User13, + User14, + User15, + ], + number_of_users=number_of_users, + ) + delta_ms = (time.perf_counter_ns() - ts) / 1e6 + self.assertEqual(sum(user_class_occurrences.values()), number_of_users) + self.assertLessEqual(delta_ms, 100) From 487873b3f55a57763e2301b243f9375dc85d36f6 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 31 May 2021 15:58:10 -0400 Subject: [PATCH 063/139] Add another assertion in distribution logic --- locust/distribution.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locust/distribution.py b/locust/distribution.py index 92101f2bee..f5374521af 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -60,11 +60,13 @@ def weight_users( return user_class_occurrences else: - return _find_ideal_users_to_add_or_remove( + user_class_occurrences = _find_ideal_users_to_add_or_remove( user_classes, number_of_users - sum(user_class_occurrences.values()), user_class_occurrences.copy(), ) + assert sum(user_class_occurrences.values()) == number_of_users + return user_class_occurrences def _find_ideal_users_to_add_or_remove( From fc4dc02772e25793f80864700e439b455fba1fca Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 2 Jun 2021 15:38:59 -0400 Subject: [PATCH 064/139] Use time.perf_counter() as time.perf_counter_ns() is not in py3.6 --- locust/test/test_distribution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 4c77229367..79fe0e69d9 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -276,7 +276,7 @@ class User15(User): weight = 69 for number_of_users in range(1044523783783, 1044523783783 + 1000): - ts = time.perf_counter_ns() + ts = time.perf_counter() user_class_occurrences = weight_users( user_classes=[ User1, @@ -297,6 +297,6 @@ class User15(User): ], number_of_users=number_of_users, ) - delta_ms = (time.perf_counter_ns() - ts) / 1e6 + delta_ms = 1e3 * (time.perf_counter() - ts) self.assertEqual(sum(user_class_occurrences.values()), number_of_users) self.assertLessEqual(delta_ms, 100) From 9656102b727f6114297b000262e376a2b08279a1 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 2 Jun 2021 16:25:51 -0400 Subject: [PATCH 065/139] Introduce WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP This fixes the issue of the master staying in the "stopping" state in certain situations. The issue was introduced by https://github.com/locustio/locust/pull/1769. --- locust/runners.py | 5 ++++ locust/test/test_runners.py | 49 +++++++++++++++++++++++++------------ 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 6172892994..12e5bb07d4 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import json import logging +import os import socket import sys import time @@ -994,6 +995,10 @@ def worker(self): elif msg.type == "stop": self.stop() self.client.send(Message("client_stopped", None, self.client_id)) + # +additional_wait is just a small buffer to account for the random network latencies and/or other + # random delays inherent to distributed systems. + additional_wait = int(os.getenv("WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", 0)) + gevent.sleep((self.environment.stop_timeout or 0) + additional_wait) self.client.send(Message("client_ready", None, self.client_id)) self.worker_state = STATE_INIT elif msg.type == "quit": diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 9284170439..5d45a85279 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -702,8 +702,9 @@ def tick(self): return None with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + stop_timeout = 20 master_env = Environment( - user_classes=[TestUser1, TestUser2, TestUser3], shape_class=TestShape(), stop_timeout=20 + user_classes=[TestUser1, TestUser2, TestUser3], shape_class=TestShape(), stop_timeout=stop_timeout ) master_env.shape_class.reset_time() master = master_env.create_master_runner("*", 0) @@ -717,7 +718,7 @@ def tick(self): # Give workers time to connect sleep(0.1) - self.assertEqual("ready", master.state) + self.assertEqual(STATE_INIT, master.state) self.assertEqual(5, len(master.clients.ready)) # Re-order `workers` so that it is sorted by `id`. @@ -730,11 +731,11 @@ def tick(self): # First stage ts = time.time() - while master.state != "spawning": + while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() sleep(5 - (time.time() - ts)) # runtime = 5s - self.assertEqual("running", master.state) + self.assertEqual(STATE_RUNNING, master.state) w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} @@ -754,11 +755,11 @@ def tick(self): # Second stage ts = time.time() - while master.state != "spawning": + while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() sleep(5 - (time.time() - ts)) # runtime = 15s - self.assertEqual("running", master.state) + self.assertEqual(STATE_RUNNING, master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} @@ -778,15 +779,15 @@ def tick(self): # Third stage ts = time.time() - while master.state != "spawning": + while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() sleep(5 - (time.time() - ts)) # runtime = 25s ts = time.time() - while master.state != "running": + while master.state != STATE_RUNNING: self.assertTrue(time.time() - ts <= 1) sleep() - self.assertEqual("running", master.state) + self.assertEqual(STATE_RUNNING, master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} @@ -806,7 +807,7 @@ def tick(self): # Fourth stage ts = time.time() - while master.state != "spawning": + while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() sleep(5 - (time.time() - ts)) # runtime = 35s @@ -814,7 +815,7 @@ def tick(self): # Fourth stage - Excess TestUser3 have been stopped but # TestUser1/TestUser2 have not reached stop timeout yet, so # their number are unchanged - self.assertEqual("spawning", master.state) + self.assertEqual(STATE_SPAWNING, master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} @@ -835,7 +836,7 @@ def tick(self): # Fourth stage - TestUser2/TestUser3 are now at the desired # number, but TestUser1 is still unchanged ts = time.time() - while master.state != "spawning": + while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() delta = time.time() - ts @@ -858,7 +859,7 @@ def tick(self): # Fourth stage - All users are now at the desired number ts = time.time() - while master.state != "running": + while master.state != STATE_RUNNING: self.assertTrue(time.time() - ts <= 1) sleep() delta = time.time() - ts @@ -881,9 +882,25 @@ def tick(self): # Sleep stop_timeout and make sure the test has stopped sleep(1) # runtime = 61s - self.assertEqual("stopping", master.state) - sleep(20) # runtime = 81s - self.assertEqual("stopped", master.state) + self.assertEqual(STATE_STOPPING, master.state) + sleep(stop_timeout) # runtime = 81s + self.assertEqual(STATE_STOPPED, master.state) + + # We wait for "stop_timeout" seconds to let the workers reconnect as "ready" with the master. + # The reason for waiting an additional "stop_timeout" when we already waited for "stop_timeout" + # above is that when a worker receives the stop message, it can take up to "stop_timeout" + # for the worker to send the "client_stopped" message then an additional "stop_timeout" seconds + # to send the "client_ready" message. + ts = time.time() + while len(master.clients.ready) != len(workers): + self.assertTrue( + time.time() - ts <= stop_timeout, + f"expected {len(workers)} workers to be ready but only {len(master.clients.ready)} workers are", + ) + sleep() + sleep(1) + + # Check that no users are running w1 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w2 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} From e6288c5ebc4d9f16cdd08135a20a4bf3400ae704 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 3 Jun 2021 10:51:35 -0400 Subject: [PATCH 066/139] Improve assertions in dispatch logic to handle lots of workers --- locust/dispatch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index b43cfd82a6..2487a99eca 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -140,6 +140,9 @@ def get_users_to_dispatch_for_current_iteration( number_of_users_in_current_dispatch = 0 for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): + # For large number of user classes and large number of workers, this assertion might fail. + # If this happens, you can remove it or increase the threshold. Right now, the assertion + # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: @@ -152,7 +155,9 @@ def get_users_to_dispatch_for_current_iteration( continue for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): - assert j < 100, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + assert j < int( + 2 * len(effective_balanced_users) + ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) if effective_balanced_users[worker_node_id][user_class] == 0: continue dispatched_users[worker_node_id][user_class] += 1 From f5fed1712425f9eb5f5a6795ddc3d77384a246ec Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 3 Jun 2021 10:52:07 -0400 Subject: [PATCH 067/139] Add debug log to show users during ramp-up --- locust/runners.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 12e5bb07d4..39bf332c98 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -662,6 +662,16 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) dispatch_greenlets.join() + logger.debug( + "Currently spawned users: %s (%i total running)" + % ( + ", ".join( + "%s: %d" % (name, count) for name, count in self.reported_user_class_occurrences.items() + ), + sum(self.reported_user_class_occurrences.values()), + ) + ) + except KeyboardInterrupt: # We need to catch keyboard interrupt. Otherwise, if KeyboardInterrupt is received while in # a gevent.sleep inside the dispatch_users function, locust won't gracefully shutdown. From 231efc36aa83902826988f04a8b034120d793e48 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 3 Jun 2021 16:50:47 -0400 Subject: [PATCH 068/139] Make TestDispatchUsersToWorkersFuzzy deterministic --- locust/test/test_dispatch.py | 94 ++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index f67d7767e2..47bde612cb 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -1,6 +1,7 @@ -import random +import itertools import time import unittest +from typing import Dict from locust.dispatch import ( all_users_have_been_dispatched, @@ -1915,40 +1916,71 @@ def test_dispatch_users_to_3_workers(self): class TestDispatchUsersToWorkersFuzzy(unittest.TestCase): def test_dispatch_users_to_workers(self): """ - This "fuzzy" test uses the "dispatch_users" with various random + This "fuzzy" test uses the "dispatch_users" with various input parameters to validate that the dispatch logic does not get stuck in some infinite loop or other unforeseen situations. """ - - for _ in range(20): - for max_prior_users in [0, 3, 5]: - number_of_user_classes = random.randint(1, 30) - number_of_workers = random.randint(1, 30) - - worker_nodes = [] - for i in range(1, number_of_workers + 1): - worker_node = WorkerNode(str(i)) - worker_node.user_class_occurrences = { - f"User{i}": random.randint(0, max_prior_users) - for i in range(1, random.randint(1, number_of_user_classes + 1)) + weights_iterator = itertools.cycle([5, 4, 9, 4, 9, 8, 4, 10, 1, 5]) + number_of_prior_users_iterator = itertools.cycle([0, 1, 3, 10, 20]) + spawn_rate_multipliers_iterator = itertools.cycle( + [ + 0.770798258958361, + 0.6310883490428525, + 0.08332730831289559, + 0.5498638520309477, + 0.33919312148903324, + 0.275113942104787, + 0.4120114294121081, + 0.9757043117340924, + 0.5950736075658479, + 0.42576147123568686, + 0.001, + ] + ) + + # We repeat the test cases thrice so that we cover enough of the + # values in the above iterators. + for _ in range(5): + for number_of_user_classes in [1, 5, 10, 20, 30, 40, 50, 100]: + for number_of_workers in [1, 2, 5, 10, 30, 80, 100, 500]: + worker_nodes = [] + for i in range(1, number_of_workers + 1): + worker_node = WorkerNode(str(i)) + worker_node.user_class_occurrences = { + f"User{i}": next(number_of_prior_users_iterator) + for i in range(1, number_of_user_classes + 1) + } + worker_nodes.append(worker_node) + + user_class_occurrences = { + f"User{i}": next(weights_iterator) for i in range(1, number_of_user_classes + 1) } - worker_nodes.append(worker_node) - - user_class_occurrences = { - f"User{i}": random.randint(0, 50) for i in range(1, number_of_user_classes + 1) - } - total_number_of_users = sum(user_class_occurrences.values()) - # We limit the maximum total dispatch to around 10s (i.e. total_number_of_users / 10) - # so that the test does take too much time. - spawn_rate = max(total_number_of_users / 10, 100 * random.random()) - - users_dispatcher = dispatch_users( - worker_nodes=worker_nodes, - user_class_occurrences=user_class_occurrences, - spawn_rate=spawn_rate, - ) - - dispatched = list(users_dispatcher) + + # We limit the maximum total dispatch to around 10s (i.e. total_number_of_users / 10) + # so that the test does take too much time. + total_number_of_users = sum(user_class_occurrences.values()) + spawn_rate = max(total_number_of_users / 10, 100 * next(spawn_rate_multipliers_iterator)) + + users_dispatcher = dispatch_users( + worker_nodes=worker_nodes, + user_class_occurrences=user_class_occurrences, + spawn_rate=spawn_rate, + ) + + # The dispatch should not take more than 20s. More than + # that would be a sign of performance issue. + ts = time.perf_counter() + dispatched = list(users_dispatcher) + self.assertLessEqual( + time.perf_counter() - ts, + 20, + "number_of_user_classes: {} - number_of_workers: {} - total_number_of_users: {} - spawn_rate: {}".format( + number_of_user_classes, + number_of_workers, + total_number_of_users, + spawn_rate, + ), + ) class TestNumberOfUsersLeftToDispatch(unittest.TestCase): From e83b166f651fd95da00e70ac0508db0acd0b5441 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 3 Jun 2021 16:52:18 -0400 Subject: [PATCH 069/139] Ensure that the distribution is respected during a ramp-up --- locust/dispatch.py | 129 +++++++++-- locust/test/test_dispatch.py | 404 ++++++++++++++++++++++++++++++++++- 2 files changed, 509 insertions(+), 24 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 2487a99eca..160e204572 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -1,5 +1,7 @@ +import functools import itertools import math +import operator import time from collections import namedtuple from copy import deepcopy @@ -135,7 +137,9 @@ def get_users_to_dispatch_for_current_iteration( effective_balanced_users: Dict[str, Dict[str, int]], number_of_users_per_dispatch: int, ) -> Dict[str, Dict[str, int]]: - ts_dispatch = time.time() + ts_dispatch = time.perf_counter() + + number_of_workers = len(effective_balanced_users) number_of_users_in_current_dispatch = 0 @@ -156,7 +160,7 @@ def get_users_to_dispatch_for_current_iteration( for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): assert j < int( - 2 * len(effective_balanced_users) + 2 * number_of_workers ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) if effective_balanced_users[worker_node_id][user_class] == 0: continue @@ -168,8 +172,15 @@ def get_users_to_dispatch_for_current_iteration( if number_of_users_in_current_dispatch == number_of_users_per_dispatch: break - assert time.time() - ts_dispatch < 0.5, "Dispatch iteration took too much time: {}s".format( - time.time() - ts_dispatch + # Another assertion to safeguard against unforeseen situations. Ideally, + # we want each dispatch loop to be as short as possible to compute, but with + # a large amount of workers/user classes, it can take longer to come up with the dispatch solution. + # If the assertion is raised, then it could be a sign that the code needs to be optimized for the + # case that caused the assertion to be raised. + assert time.perf_counter() - ts_dispatch < ( + 0.5 if number_of_workers < 100 else 1 if number_of_workers < 250 else 1.5 if number_of_workers < 350 else 3 + ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( + time.perf_counter() - ts_dispatch, number_of_workers, len(user_class_occurrences) ) return { @@ -179,7 +190,7 @@ def get_users_to_dispatch_for_current_iteration( def go_to_next_user_class( - user_class: str, + current_user_class: str, user_class_occurrences: Dict[str, int], dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], @@ -190,19 +201,111 @@ def go_to_next_user_class( a ramp up. """ dispatched_user_class_occurrences = { - user_class_: sum(x[user_class_] for x in dispatched_users.values()) - for user_class_ in user_class_occurrences.keys() + user_class: sum(x[user_class] for x in dispatched_users.values()) + for user_class in user_class_occurrences.keys() } - for user_class_ in sorted(user_class_occurrences.keys()): - if user_class_ == user_class: - continue - if sum(x[user_class_] for x in effective_balanced_users.values()) == 0: - continue - if dispatched_user_class_occurrences[user_class] - dispatched_user_class_occurrences[user_class_] >= 1: + if all(n > 0 for n in dispatched_user_class_occurrences.values()): + if not current_user_class_will_keep_distribution_better_than_all_other_user_classes( + current_user_class, user_class_occurrences, dispatched_user_class_occurrences + ): return True + else: + return False + else: + # Because each user class doesn't have at least one user, we use a simpler strategy + # that make sure the each user class appears once. + for user_class in filter( + functools.partial(operator.ne, current_user_class), sorted(user_class_occurrences.keys()) + ): + if sum(x[user_class] for x in effective_balanced_users.values()) == 0: + # No more users of class `user_class` to dispatch + continue + if ( + dispatched_user_class_occurrences[current_user_class] - dispatched_user_class_occurrences[user_class] + >= 1 + ): + # There's already enough `current_user_class` for the current dispatch. Hence, we should + # not consider `current_user_class` and go to the next user class instead. + return True + return False + + +def current_user_class_will_keep_distribution_better_than_all_other_user_classes( + current_user_class: str, + user_class_occurrences: Dict[str, int], + dispatched_user_class_occurrences: Dict[str, int], +) -> bool: + distances = get_distances_from_ideal_distribution( + current_user_class, user_class_occurrences, dispatched_user_class_occurrences + ) + if distances.actual_distance_with_current_user_class > distances.actual_distance and all( + not current_user_class_will_keep_distribution( + user_class, user_class_occurrences, dispatched_user_class_occurrences + ) + for user_class in user_class_occurrences.keys() + if user_class != current_user_class + ): + # If we are here, it means that if one user of `current_user_class` is added + # then the distribution will be the best we can get. In other words, adding + # one user of any other user class won't yield a better distribution. + return True + if distances.actual_distance_with_current_user_class <= distances.actual_distance: + return True + return False + + +def current_user_class_will_keep_distribution( + current_user_class: str, + user_class_occurrences: Dict[str, int], + dispatched_user_class_occurrences: Dict[str, int], +) -> bool: + distances = get_distances_from_ideal_distribution( + current_user_class, user_class_occurrences, dispatched_user_class_occurrences + ) + if distances.actual_distance_with_current_user_class <= distances.actual_distance: + return True return False +# `actual_distance` corresponds to the distance from the ideal distribution for the current +# dispatched users. `actual_distance_with_current_user_class` represents the distance +# from the ideal distribution if we were to add one user of the given `current_user_class`. +# Thus, we strive to find the right user class to add a user in that will give us +# a `actual_distance_with_current_user_class` less than `actual_distance`. +DistancesFromIdealDistribution = namedtuple( + "DistancesFromIdealDistribution", "actual_distance actual_distance_with_current_user_class" +) + + +def get_distances_from_ideal_distribution( + current_user_class: str, + user_class_occurrences: Dict[str, int], + dispatched_user_class_occurrences: Dict[str, int], +) -> DistancesFromIdealDistribution: + user_classes = sorted(user_class_occurrences.keys()) + desired_weights = [ + user_class_occurrences[user_class] / sum(user_class_occurrences.values()) for user_class in user_classes + ] + actual_weights = [ + dispatched_user_class_occurrences[user_class] / sum(dispatched_user_class_occurrences.values()) + for user_class in user_classes + ] + actual_weights_with_current_user_class = [ + ( + dispatched_user_class_occurrences[user_class] + 1 + if user_class == current_user_class + else dispatched_user_class_occurrences[user_class] + ) + / (sum(dispatched_user_class_occurrences.values()) + 1) + for user_class in user_classes + ] + actual_distance = math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, desired_weights)))) + actual_distance_with_current_user_class = math.sqrt( + sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights_with_current_user_class, desired_weights))) + ) + return DistancesFromIdealDistribution(actual_distance, actual_distance_with_current_user_class) + + def number_of_users_left_to_dispatch( dispatched_users: Dict[str, Dict[str, int]], balanced_users: Dict[str, Dict[str, int]], diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 47bde612cb..f2346cb5fe 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -1528,7 +1528,19 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, @@ -1583,7 +1595,19 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(1.98 <= delta <= 2.02, delta) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, @@ -1638,7 +1662,19 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, @@ -1681,14 +1717,26 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 1}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1726,14 +1774,26 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, + "3": {"User1": 0, "User2": 7, "User3": 1}, + }, + ) + delta = time.time() - ts + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1913,6 +1973,321 @@ def test_dispatch_users_to_3_workers(self): self.assertTrue(0 <= delta <= 0.02, delta) +class TestDistributionIsKeptDuringDispatch(unittest.TestCase): + def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): + """ + Test case covering reported issue in https://github.com/locustio/locust/pull/1621#issuecomment-853624275. + + The case is to ramp-up from 0 to 75 users with two user classes. `User1` has a weight of 1 and `User2` + has a weight of 2. The original issue was with 500 users, but to keep the test shorter, we use 75 users. + """ + + def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: + user_classes = list(next(iter(d.values())).keys()) + return {u: sum(d[u] for d in d.values()) for u in user_classes} + + worker_node1 = WorkerNode("1") + worker_node2 = WorkerNode("2") + worker_node3 = WorkerNode("3") + worker_node4 = WorkerNode("4") + + users_dispatcher = dispatch_users( + worker_nodes=[worker_node1, worker_node2, worker_node3, worker_node4], + user_class_occurrences={"User1": 25, "User2": 50}, + spawn_rate=5, + ) + + # total user count = 5 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 2, "User2": 3}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 2, "User2": 3}, + "2": {"User1": 0, "User2": 0}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + # total user count = 10 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 4, "User2": 6}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 4, "User2": 6}, + "2": {"User1": 0, "User2": 0}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 15 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 5, "User2": 10}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 5, "User2": 10}, + "2": {"User1": 0, "User2": 0}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 20 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 7, "User2": 13}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 0, "User2": 1}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 25 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 9, "User2": 16}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 2, "User2": 4}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 30 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 10, "User2": 20}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 3, "User2": 8}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 35 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 12, "User2": 23}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 5, "User2": 11}, + "3": {"User1": 0, "User2": 0}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 40 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 14, "User2": 26}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 1, "User2": 1}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 45 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 15, "User2": 30}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 2, "User2": 5}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 50 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 17, "User2": 33}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 4, "User2": 8}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 55 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 19, "User2": 36}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 6, "User2": 11}, + "4": {"User1": 0, "User2": 0}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 60 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 20, "User2": 40}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 6, "User2": 13}, + "4": {"User1": 1, "User2": 2}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 65 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 22, "User2": 43}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 6, "User2": 13}, + "4": {"User1": 3, "User2": 5}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 70 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 24, "User2": 46}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 6, "User2": 13}, + "4": {"User1": 5, "User2": 8}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + # total user count = 75, User1 = 25, User2 = 50 + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 25, "User2": 50}, + ) + self.assertDictEqual( + dispatched_users, + { + "1": {"User1": 7, "User2": 12}, + "2": {"User1": 6, "User2": 13}, + "3": {"User1": 6, "User2": 13}, + "4": {"User1": 6, "User2": 12}, + }, + ) + delta = time.time() - ts + self.assertTrue(0.98 <= delta <= 1.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + class TestDispatchUsersToWorkersFuzzy(unittest.TestCase): def test_dispatch_users_to_workers(self): """ @@ -1967,13 +2342,20 @@ def test_dispatch_users_to_workers(self): spawn_rate=spawn_rate, ) - # The dispatch should not take more than 20s. More than - # that would be a sign of performance issue. ts = time.perf_counter() - dispatched = list(users_dispatcher) + list(users_dispatcher) + expected_spawn_duration = ( + 10 + if number_of_workers < 100 + else 15 + if number_of_workers < 250 + else 20 + if number_of_workers < 400 + else 35 + ) self.assertLessEqual( time.perf_counter() - ts, - 20, + expected_spawn_duration, "number_of_user_classes: {} - number_of_workers: {} - total_number_of_users: {} - spawn_rate: {}".format( number_of_user_classes, number_of_workers, From ed4b3b381f42c7409a7f43aa6cad3cfc5d6c9a44 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 4 Jun 2021 09:50:59 -0400 Subject: [PATCH 070/139] Use time.perf_counter() instead of time.time() in dispatch --- locust/dispatch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 160e204572..3d8e6b1e5e 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -98,10 +98,10 @@ def dispatch_users( user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch ) - ts = time.time() + ts = time.perf_counter() yield users_to_dispatch if sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - delta = time.time() - ts + delta = time.perf_counter() - ts sleep_duration = max(0.0, wait_between_dispatch - delta) assert sleep_duration <= 10, sleep_duration gevent.sleep(sleep_duration) @@ -118,9 +118,9 @@ def dispatch_users( user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch ) - ts = time.time() + ts = time.perf_counter() yield users_to_dispatch - delta = time.time() - ts + delta = time.perf_counter() - ts sleep_duration = max(0.0, wait_between_dispatch - delta) assert sleep_duration <= 10, sleep_duration gevent.sleep(sleep_duration) From cc4a208f0346ec3b70ade51adccf6072ea04d6ba Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 4 Jun 2021 09:51:49 -0400 Subject: [PATCH 071/139] Optimize hotspots in dispatch.py using line_profiler --- locust/dispatch.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 3d8e6b1e5e..d6a0b38abf 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -201,7 +201,7 @@ def go_to_next_user_class( a ramp up. """ dispatched_user_class_occurrences = { - user_class: sum(x[user_class] for x in dispatched_users.values()) + user_class: sum(map(operator.itemgetter(user_class), dispatched_users.values())) for user_class in user_class_occurrences.keys() } if all(n > 0 for n in dispatched_user_class_occurrences.values()): @@ -217,7 +217,7 @@ def go_to_next_user_class( for user_class in filter( functools.partial(operator.ne, current_user_class), sorted(user_class_occurrences.keys()) ): - if sum(x[user_class] for x in effective_balanced_users.values()) == 0: + if sum(map(operator.itemgetter(user_class), effective_balanced_users.values())) == 0: # No more users of class `user_class` to dispatch continue if ( @@ -365,12 +365,14 @@ def balance_users_among_workers( break if ( sum(balanced_users[worker_node.id].values()) == users_per_worker - and total_users - sum(map(sum, map(lambda x: x.values(), balanced_users.values()))) > remainder + and total_users - sum(map(sum, map(operator.methodcaller("values"), balanced_users.values()))) + > remainder ): continue elif ( sum(balanced_users[worker_node.id].values()) == users_per_worker + 1 - and total_users - sum(map(sum, map(lambda x: x.values(), balanced_users.values()))) < remainder + and total_users - sum(map(sum, map(operator.methodcaller("values"), balanced_users.values()))) + < remainder ): continue balanced_users[worker_node.id][user_class] += 1 From d79879637619d54ef43e3ab0d74a44d87ab16109 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 9 Jun 2021 12:04:14 -0400 Subject: [PATCH 072/139] Use fractions instead of percentages --- locust/distribution.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index f5374521af..f1e75f263c 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -117,19 +117,19 @@ def distance_from_desired_distribution( user_classes: List[Type[User]], user_class_occurrences: Dict[str, int], ) -> float: - user_class_2_actual_percentage = { - user_class: 100 * occurrences / sum(user_class_occurrences.values()) + user_class_2_actual_ratio = { + user_class: occurrences / sum(user_class_occurrences.values()) for user_class, occurrences in user_class_occurrences.items() } - user_class_2_expected_percentage = { - user_class.__name__: 100 * user_class.weight / sum(map(attrgetter("weight"), user_classes)) + user_class_2_expected_ratio = { + user_class.__name__: user_class.weight / sum(map(attrgetter("weight"), user_classes)) for user_class in user_classes } differences = [ - user_class_2_actual_percentage[user_class] - expected_percentage - for user_class, expected_percentage in user_class_2_expected_percentage.items() + user_class_2_actual_ratio[user_class] - expected_ratio + for user_class, expected_ratio in user_class_2_expected_ratio.items() ] return math.sqrt(math.fsum(map(lambda x: x ** 2, differences))) From 71a1aa48f0eddee8387bfb8401dc3438a527e423 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 9 Jun 2021 12:06:18 -0400 Subject: [PATCH 073/139] Remove `get_` prefix in function name --- locust/dispatch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index d6a0b38abf..5c868e5206 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -94,7 +94,7 @@ def dispatch_users( if less_users_than_desired: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - users_to_dispatch = get_users_to_dispatch_for_current_iteration( + users_to_dispatch = users_to_dispatch_for_current_iteration( user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch ) @@ -114,7 +114,7 @@ def dispatch_users( else: while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): - users_to_dispatch = get_users_to_dispatch_for_current_iteration( + users_to_dispatch = users_to_dispatch_for_current_iteration( user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch ) @@ -131,7 +131,7 @@ def dispatch_users( yield balanced_users -def get_users_to_dispatch_for_current_iteration( +def users_to_dispatch_for_current_iteration( user_class_occurrences: Dict[str, int], dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], From 81f6d66cf69755ee966a26741e251ac560c78233 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 9 Jun 2021 18:05:04 -0400 Subject: [PATCH 074/139] Ensure that the exception objects are not mutated when generated report Cherry-picked from 60cde73cee8c08bd5c6efb88eb0d511c6c025620 --- locust/html.py | 5 ++++- locust/test/test_web.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/locust/html.py b/locust/html.py index 34ecfcd3bf..0ec71c5d22 100644 --- a/locust/html.py +++ b/locust/html.py @@ -1,3 +1,5 @@ +from copy import deepcopy + from jinja2 import Environment, FileSystemLoader import os import pathlib @@ -36,7 +38,8 @@ def get_html_report(environment, show_download_link=True): requests_statistics = list(chain(sort_stats(stats.entries), [stats.total])) failures_statistics = sort_stats(stats.errors) exceptions_statistics = [] - for exc in environment.runner.exceptions.values(): + exceptions = deepcopy(environment.runner.exceptions) + for exc in exceptions.values(): exc["nodes"] = ", ".join(exc["nodes"]) exceptions_statistics.append(exc) diff --git a/locust/test/test_web.py b/locust/test/test_web.py index 86c1ba2a70..6f4f87f14c 100644 --- a/locust/test/test_web.py +++ b/locust/test/test_web.py @@ -354,6 +354,14 @@ def test_report_exceptions(self): # self.assertEqual(200, r.status_code) self.assertIn("

Exceptions Statistics

", r.text) + # Prior to 088a98bf8ff4035a0de3becc8cd4e887d618af53, the "nodes" field for each exception in + # "self.runner.exceptions" was accidentally mutated in "get_html_report" to a string. + # This assertion reproduces the issue and it is left there to make sure there's no + # regression in the future. + self.assertTrue( + isinstance(next(iter(self.runner.exceptions.values()))["nodes"], set), "exception object has been mutated" + ) + class TestWebUIAuth(LocustTestCase): def setUp(self): From 10ed15bddd01eda058cd918f2d7c0aed43ee5ce2 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 10 Jun 2021 22:43:11 -0400 Subject: [PATCH 075/139] Fix bug & improve logic in dispatcher - A bug caused the dispatcher to not obey the spawn rate when users were already running. For instance, going from 25 to 50 at 1/s with 20 workers was done in only one second, i.e. an instantaneous increase of 25 -> 50. The fix makes sure that the spawn rate is always respected. - The dispatcher incorrectly handled when one or more user classes had more users running than what is desired, Prior to this fix, the dispatcher would add user to user classes already in excess. Now, the dispatcher only adds users to user classes with a user count below the desired count. **NOTE**: The dispatch module needs some cleanup which will be included in future commits. --- locust/dispatch.py | 137 +++++++++++++++----------- locust/test/test_dispatch.py | 184 +++++++++++++---------------------- locust/test/test_runners.py | 92 +++++++++--------- 3 files changed, 192 insertions(+), 221 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 5c868e5206..94481bb488 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -95,7 +95,11 @@ def dispatch_users( if less_users_than_desired: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: users_to_dispatch = users_to_dispatch_for_current_iteration( - user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch + user_class_occurrences, + dispatched_users, + effective_balanced_users, + balanced_users, + number_of_users_per_dispatch, ) ts = time.perf_counter() @@ -113,75 +117,101 @@ def dispatch_users( yield balanced_users else: - while not all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences): + while not all_users_have_been_dispatched(effective_balanced_users): users_to_dispatch = users_to_dispatch_for_current_iteration( - user_class_occurrences, dispatched_users, effective_balanced_users, number_of_users_per_dispatch + user_class_occurrences, + dispatched_users, + effective_balanced_users, + balanced_users, + number_of_users_per_dispatch, ) ts = time.perf_counter() yield users_to_dispatch delta = time.perf_counter() - ts - sleep_duration = max(0.0, wait_between_dispatch - delta) + sleep_duration = ( + max(0.0, wait_between_dispatch - delta) + if not all_users_have_been_dispatched(effective_balanced_users) + else 0 + ) assert sleep_duration <= 10, sleep_duration gevent.sleep(sleep_duration) - # If we are here, it means we have an excess of users for one or more user classes. - # Hence, we need to dispatch a last set of users that will bring the users - # distribution to the desired one. - yield balanced_users - def users_to_dispatch_for_current_iteration( user_class_occurrences: Dict[str, int], dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], + balanced_users: Dict[str, Dict[str, int]], number_of_users_per_dispatch: int, ) -> Dict[str, Dict[str, int]]: - ts_dispatch = time.perf_counter() + if all( + sum(map(operator.itemgetter(user_class), dispatched_users.values())) >= user_count + for user_class, user_count in user_class_occurrences.items() + ): + dispatched_users.update(balanced_users) + effective_balanced_users.update( + { + worker_node_id: {user_class: 0 for user_class in user_class_occurrences.keys()} + for worker_node_id, user_class_occurrences in dispatched_users.items() + } + ) - number_of_workers = len(effective_balanced_users) + else: + ts_dispatch = time.perf_counter() - number_of_users_in_current_dispatch = 0 + number_of_workers = len(effective_balanced_users) - for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): - # For large number of user classes and large number of workers, this assertion might fail. - # If this happens, you can remove it or increase the threshold. Right now, the assertion - # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). - assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) + number_of_users_in_current_dispatch = 0 - if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: - break + for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): + # For large number of user classes and large number of workers, this assertion might fail. + # If this happens, you can remove it or increase the threshold. Right now, the assertion + # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). + assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) - if all_users_of_current_class_have_been_dispatched(effective_balanced_users, user_class): - continue + if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: + break - if go_to_next_user_class(user_class, user_class_occurrences, dispatched_users, effective_balanced_users): - continue + if all( + sum(map(operator.itemgetter(user_class_), dispatched_users.values())) >= user_count + for user_class_, user_count in user_class_occurrences.items() + ): + break - for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): - assert j < int( - 2 * number_of_workers - ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if effective_balanced_users[worker_node_id][user_class] == 0: + if ( + sum(map(operator.itemgetter(user_class), dispatched_users.values())) + >= user_class_occurrences[user_class] + ): continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 - number_of_users_in_current_dispatch += 1 - break - if number_of_users_in_current_dispatch == number_of_users_per_dispatch: - break + if go_to_next_user_class(user_class, user_class_occurrences, dispatched_users, effective_balanced_users): + continue - # Another assertion to safeguard against unforeseen situations. Ideally, - # we want each dispatch loop to be as short as possible to compute, but with - # a large amount of workers/user classes, it can take longer to come up with the dispatch solution. - # If the assertion is raised, then it could be a sign that the code needs to be optimized for the - # case that caused the assertion to be raised. - assert time.perf_counter() - ts_dispatch < ( - 0.5 if number_of_workers < 100 else 1 if number_of_workers < 250 else 1.5 if number_of_workers < 350 else 3 - ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( - time.perf_counter() - ts_dispatch, number_of_workers, len(user_class_occurrences) - ) + for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): + assert j < int( + 2 * number_of_workers + ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + if effective_balanced_users[worker_node_id][user_class] == 0: + continue + dispatched_users[worker_node_id][user_class] += 1 + effective_balanced_users[worker_node_id][user_class] -= 1 + number_of_users_in_current_dispatch += 1 + break + + if number_of_users_in_current_dispatch == number_of_users_per_dispatch: + break + + # Another assertion to safeguard against unforeseen situations. Ideally, + # we want each dispatch loop to be as short as possible to compute, but with + # a large amount of workers/user classes, it can take longer to come up with the dispatch solution. + # If the assertion is raised, then it could be a sign that the code needs to be optimized for the + # case that caused the assertion to be raised. + assert time.perf_counter() - ts_dispatch < ( + 0.5 if number_of_workers < 100 else 1 if number_of_workers < 250 else 1.5 if number_of_workers < 350 else 3 + ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( + time.perf_counter() - ts_dispatch, number_of_workers, len(user_class_occurrences) + ) return { worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) @@ -320,25 +350,14 @@ def number_of_users_left_to_dispatch( ) -def all_users_have_been_dispatched( - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], - user_class_occurrences: Dict[str, int], -) -> bool: +def all_users_have_been_dispatched(effective_balanced_users: Dict[str, Dict[str, int]]) -> bool: return all( - sum(x[user_class] for x in dispatched_users.values()) - >= sum(x[user_class] for x in effective_balanced_users.values()) - for user_class in user_class_occurrences.keys() + user_count == 0 + for user_class_occurrences in effective_balanced_users.values() + for user_count in user_class_occurrences.values() ) -def all_users_of_current_class_have_been_dispatched( - effective_balanced_users: Dict[str, Dict[str, int]], - user_class: str, -) -> bool: - return all(x[user_class] == 0 for x in effective_balanced_users.values()) - - def balance_users_among_workers( worker_nodes, # type: List[WorkerNode] user_class_occurrences: Dict[str, int], diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index f2346cb5fe..323a0a9e03 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -4,8 +4,6 @@ from typing import Dict from locust.dispatch import ( - all_users_have_been_dispatched, - all_users_of_current_class_have_been_dispatched, balance_users_among_workers, dispatch_users, number_of_users_left_to_dispatch, @@ -1528,8 +1526,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) @@ -1540,9 +1538,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 1}, }, ) delta = time.time() - ts @@ -1595,8 +1593,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) @@ -1607,9 +1605,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 1}, }, ) delta = time.time() - ts @@ -1662,8 +1660,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) @@ -1674,9 +1672,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "3": {"User1": 0, "User2": 7, "User3": 1}, }, ) delta = time.time() - ts @@ -1717,8 +1715,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) @@ -1729,7 +1727,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 1}, }, @@ -1774,8 +1772,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 0}, }, ) @@ -1786,7 +1784,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 0, "User3": 1}, + "1": {"User1": 0, "User2": 0, "User3": 1}, "2": {"User1": 5, "User2": 0, "User3": 1}, "3": {"User1": 0, "User2": 7, "User3": 1}, }, @@ -1981,11 +1979,6 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): The case is to ramp-up from 0 to 75 users with two user classes. `User1` has a weight of 1 and `User2` has a weight of 2. The original issue was with 500 users, but to keep the test shorter, we use 75 users. """ - - def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: - user_classes = list(next(iter(d.values())).keys()) - return {u: sum(d[u] for d in d.values()) for u in user_classes} - worker_node1 = WorkerNode("1") worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") @@ -2288,6 +2281,54 @@ def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: self.assertTrue(0 <= delta <= 0.02, delta) +class TestDispatch(unittest.TestCase): + """ + Class containing miscellaneous tests that were used to debug some issues observed when running real tests. + """ + + def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spawn_rate_of_1(self): + """ + Prior to 81f6d66cf69755ee966a26741e251ac560c78233, the dispatcher would have directly go from 25 to 50 users + in one second due to bugs in the dispatcher. This test ensures that this problem can't + reappear in the future. + """ + worker_nodes = [WorkerNode(str(i)) for i in range(1, 21)] + + for worker_node in worker_nodes: + worker_node.user_class_occurrences = {"User1": 0} + + worker_nodes_iterator = itertools.cycle(worker_nodes) + + user_count = 0 + while user_count < 25: + next(worker_nodes_iterator).user_class_occurrences["User1"] += 1 + user_count += 1 + + users_dispatcher = dispatch_users( + worker_nodes=worker_nodes, + user_class_occurrences={"User1": 50}, + spawn_rate=1, + ) + + for dispatch_iteration in range(25): + ts = time.time() + dispatched_users = next(users_dispatcher) + self.assertDictEqual( + _aggregate_dispatched_users(dispatched_users), + {"User1": 25 + dispatch_iteration + 1}, + ) + delta = time.time() - ts + if dispatch_iteration == 0: + self.assertTrue(0 <= delta <= 0.02, delta) + else: + self.assertTrue(0.98 <= delta <= 1.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + class TestDispatchUsersToWorkersFuzzy(unittest.TestCase): def test_dispatch_users_to_workers(self): """ @@ -2402,93 +2443,6 @@ def test_number_of_users_left_to_dispatch(self): self.assertEqual(0, result) -class AllUsersHaveBeenDispatched(unittest.TestCase): - def test_all_users_have_been_dispatched(self): - user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} - effective_balanced_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - - dispatched_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - self.assertTrue( - all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) - ) - - dispatched_users = { - "Worker1": {"User1": 4, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - self.assertTrue( - all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) - ) - - dispatched_users = { - "Worker1": {"User1": 2, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - self.assertFalse( - all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) - ) - - dispatched_users = { - "Worker1": {"User1": 0, "User2": 0, "User3": 0}, - "Worker2": {"User1": 0, "User2": 0, "User3": 0}, - } - self.assertFalse( - all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) - ) - - dispatched_users = { - "Worker1": {"User1": 4, "User2": 0, "User3": 0}, - "Worker2": {"User1": 4, "User2": 0, "User3": 0}, - } - self.assertFalse( - all_users_have_been_dispatched(dispatched_users, effective_balanced_users, user_class_occurrences) - ) - - -class TestAllUsersOfCurrentClassHaveBeenDispatched(unittest.TestCase): - def test_all_users_of_current_class_have_been_dispatched(self): - effective_balanced_users = { - "Worker1": {"User1": 0, "User2": 0, "User3": 0}, - "Worker2": {"User1": 0, "User2": 0, "User3": 0}, - } - self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - - effective_balanced_users = { - "Worker1": {"User1": 1, "User2": 0, "User3": 1}, - "Worker2": {"User1": 0, "User2": 1, "User3": 0}, - } - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - - effective_balanced_users = { - "Worker1": {"User1": 0, "User2": 1, "User3": 4}, - "Worker2": {"User1": 0, "User2": 1, "User3": 4}, - } - self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - - effective_balanced_users = { - "Worker1": {"User1": 1, "User2": 1, "User3": 1}, - "Worker2": {"User1": 0, "User2": 0, "User3": 0}, - } - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) - - effective_balanced_users = { - "Worker1": {"User1": 4, "User2": 0, "User3": 0}, - "Worker2": {"User1": 4, "User2": 0, "User3": 0}, - } - self.assertFalse(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User1")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User2")) - self.assertTrue(all_users_of_current_class_have_been_dispatched(effective_balanced_users, "User3")) +def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: + user_classes = list(next(iter(d.values())).keys()) + return {u: sum(d[u] for d in d.values()) for u in user_classes} diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 5d45a85279..d115384697 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1,6 +1,8 @@ +import os import time import unittest from collections import defaultdict +from contextlib import contextmanager import gevent import mock @@ -667,7 +669,7 @@ def start(self, group: Group): @task def my_task(self): - gevent.sleep(60) + gevent.sleep(0) class TestUser2(User): def start(self, group: Group): @@ -676,7 +678,7 @@ def start(self, group: Group): @task def my_task(self): - gevent.sleep(15) + gevent.sleep(600) class TestUser3(User): def start(self, group: Group): @@ -685,7 +687,7 @@ def start(self, group: Group): @task def my_task(self): - gevent.sleep(5) + gevent.sleep(600) class TestShape(LoadTestShape): def tick(self): @@ -694,15 +696,18 @@ def tick(self): return 5, 3 elif run_time < 20: return 10, 3 - elif run_time < 30: + elif run_time < 40: return 15, 3 elif run_time < 60: return 5, 3 else: return None - with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): - stop_timeout = 20 + worker_additional_wait_before_ready_after_stop = 5 + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3), _patch_env( + "WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", str(worker_additional_wait_before_ready_after_stop) + ): + stop_timeout = 5 master_env = Environment( user_classes=[TestUser1, TestUser2, TestUser3], shape_class=TestShape(), stop_timeout=stop_timeout ) @@ -782,7 +787,7 @@ def tick(self): while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() - sleep(5 - (time.time() - ts)) # runtime = 25s + sleep(10 - (time.time() - ts)) # runtime = 30s ts = time.time() while master.state != STATE_RUNNING: self.assertTrue(time.time() - ts <= 1) @@ -803,48 +808,24 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(5 - (time.time() - ts)) # runtime = 30s + sleep(10 - (time.time() - ts)) # runtime = 40s # Fourth stage ts = time.time() while master.state != STATE_SPAWNING: self.assertTrue(time.time() - ts <= 1) sleep() - sleep(5 - (time.time() - ts)) # runtime = 35s + sleep(5 - (time.time() - ts)) # runtime = 45s - # Fourth stage - Excess TestUser3 have been stopped but - # TestUser1/TestUser2 have not reached stop timeout yet, so + # Fourth stage - Excess TestUser1 have been stopped but + # TestUser2/TestUser3 have not reached stop timeout yet, so # their number are unchanged - self.assertEqual(STATE_SPAWNING, master.state) - w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(10) # runtime = 45s - - # Fourth stage - TestUser2/TestUser3 are now at the desired - # number, but TestUser1 is still unchanged - ts = time.time() - while master.state != STATE_SPAWNING: - self.assertTrue(time.time() - ts <= 1) - sleep() - delta = time.time() - ts - w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} - w2 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} - w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 1} - w5 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 1} + self.assertEqual(master.state, STATE_SPAWNING) + w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} + w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} + w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} + w4 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} + w5 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} self.assertDictEqual(w1, workers[0].user_class_occurrences) self.assertDictEqual(w2, workers[1].user_class_occurrences) self.assertDictEqual(w3, workers[2].user_class_occurrences) @@ -855,7 +836,7 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(5 - delta) # runtime = 50s + sleep(1) # runtime = 46s # Fourth stage - All users are now at the desired number ts = time.time() @@ -878,13 +859,12 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) - sleep(10 - delta) # runtime = 60s + sleep(10 - delta) # runtime = 56s # Sleep stop_timeout and make sure the test has stopped - sleep(1) # runtime = 61s + sleep(5) # runtime = 61s self.assertEqual(STATE_STOPPING, master.state) - sleep(stop_timeout) # runtime = 81s - self.assertEqual(STATE_STOPPED, master.state) + sleep(stop_timeout) # runtime = 66s # We wait for "stop_timeout" seconds to let the workers reconnect as "ready" with the master. # The reason for waiting an additional "stop_timeout" when we already waited for "stop_timeout" @@ -894,7 +874,7 @@ def tick(self): ts = time.time() while len(master.clients.ready) != len(workers): self.assertTrue( - time.time() - ts <= stop_timeout, + time.time() - ts <= stop_timeout + worker_additional_wait_before_ready_after_stop, f"expected {len(workers)} workers to be ready but only {len(master.clients.ready)} workers are", ) sleep() @@ -917,6 +897,11 @@ def tick(self): self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + ts = time.time() + while master.state != STATE_STOPPED: + self.assertTrue(time.time() - ts <= 5, master.state) + sleep() + def test_distributed_shape_stop_and_restart(self): """ Test stopping and then restarting a LoadTestShape @@ -1688,6 +1673,19 @@ def my_task(self): self.assertEqual(3, master.spawn_rate) +@contextmanager +def _patch_env(name: str, value: str): + prev_value = os.getenv(name) + os.environ[name] = value + try: + yield + finally: + if prev_value is None: + os.unsetenv(name) + else: + os.environ[name] = prev_value + + class TestWorkerRunner(LocustTestCase): def setUp(self): super().setUp() From a9b581c73fa0a416fd2a2a172d0d0b6bf0b1e195 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sat, 12 Jun 2021 15:45:21 -0400 Subject: [PATCH 076/139] Don't copy user_class_occurrences as it is copied inside the function --- locust/distribution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/distribution.py b/locust/distribution.py index f1e75f263c..2bc5e8c3de 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -63,7 +63,7 @@ def weight_users( user_class_occurrences = _find_ideal_users_to_add_or_remove( user_classes, number_of_users - sum(user_class_occurrences.values()), - user_class_occurrences.copy(), + user_class_occurrences, ) assert sum(user_class_occurrences.values()) == number_of_users return user_class_occurrences From 75565d3c10d057b063301acf21a87aa98626de96 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sat, 12 Jun 2021 16:01:23 -0400 Subject: [PATCH 077/139] Apply formatting, review wording in comments, rename variables --- locust/dispatch.py | 121 ++++++++++++++++++++--------------- locust/test/test_dispatch.py | 1 - 2 files changed, 69 insertions(+), 53 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 94481bb488..29d14284d7 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -1,10 +1,14 @@ import functools import itertools import math -import operator import time from collections import namedtuple from copy import deepcopy +from operator import ( + itemgetter, + methodcaller, + ne, +) from typing import ( Dict, Generator, @@ -25,30 +29,32 @@ def dispatch_users( ) -> Generator[Dict[str, Dict[str, int]], None, None]: """ Generator function that dispatches the users - in `user_class_occurrences` to the workers. - The currently running users is also taken into + contained `user_class_occurrences` to the workers. + The users already running on the workers are also taken into account. - It waits an appropriate amount of time between each iteration - in order for the spawn rate to be respected, whether running in + The dispatcher waits an appropriate amount of time between each iteration + in order for the spawn rate to be respected whether running in local or distributed mode. - The spawn rate is only applicable when additional users are needed. - Hence, if `user_class_occurrences` contains less users than there are - currently running, this function won't wait and will only run for - one iteration. The logic for not stopping users at a rate of `spawn_rate` + The spawn rate is only applied when additional users are needed. + Hence, if `user_class_occurrences` contains less users than the ones running right now, + the dispatcher won't wait and will only run for + one iteration. The rationale for not stopping users at a rate of `spawn_rate` is that stopping them is a blocking operation, especially when - having a `stop_timeout` and users with tasks running for a few seconds or - more. If we were to dispatch multiple spawn messages to have a ramp down, - we'd run into problems where the previous spawning would be killed - by the new message. See the call to `self.spawning_greenlet.kill()` in - `:py:meth:`locust.runners.LocalRunner.start` and `:py:meth:`locust.runners.WorkerRunner.worker`. + a `stop_timeout` is specified. When a stop timeout is specified combined with users having long-running tasks, + attempting to to stop the users at `spawn_rate` will lead to weird behaviours (users being killed even though the + stop timeout is not reached yey). :param worker_nodes: List of worker nodes :param user_class_occurrences: Desired number of users for each class :param spawn_rate: The spawn rate """ - # Get repeatable behaviour. + # NOTE: We use "sorted" in some places in this module. It is done to ensure repeatable behaviour. + # This is especially important when iterating over a dictionary which, prior to py3.7, was + # completely unordered. For >=Py3.7, a dictionary keeps the insertion order. Even then, + # it is safer to sort the keys when repeatable behaviour is required. + worker_nodes = sorted(worker_nodes, key=lambda w: w.id) # This represents the already running users among the workers @@ -66,7 +72,11 @@ def dispatch_users( user_class_occurrences, ) - # This represents the desired users distribution minus the already running users among the workers + # This represents the desired users distribution minus the already running users among the workers. + # The values inside this dictionary are updated during the current dispatch cycle. For example, + # if we dispatch 1 user of UserClass1 to worker 1, then we will decrement by 1 the user count + # for UserClass1 of worker 1. Naturally, the current dispatch cycle is done once all the values + # reach zero. effective_balanced_users = { worker_node.id: { user_class: max( @@ -82,6 +92,10 @@ def dispatch_users( wait_between_dispatch = number_of_users_per_dispatch / spawn_rate + # We use deepcopy because we will update the values inside `dispatched_users` + # to keep track of the number of dispatched users for the current dispatch cycle. + # It is essentially the same thing as for the `effective_balanced_users` dictionary, + # but in reverse. dispatched_users = deepcopy(initial_dispatched_users) # The amount of users in each user class @@ -129,11 +143,11 @@ def dispatch_users( ts = time.perf_counter() yield users_to_dispatch delta = time.perf_counter() - ts - sleep_duration = ( - max(0.0, wait_between_dispatch - delta) - if not all_users_have_been_dispatched(effective_balanced_users) - else 0 - ) + if not all_users_have_been_dispatched(effective_balanced_users): + sleep_duration = max(0.0, wait_between_dispatch - delta) + else: + # We don't need to sleep if there's no more dispatch iteration + sleep_duration = 0 assert sleep_duration <= 10, sleep_duration gevent.sleep(sleep_duration) @@ -146,7 +160,7 @@ def users_to_dispatch_for_current_iteration( number_of_users_per_dispatch: int, ) -> Dict[str, Dict[str, int]]: if all( - sum(map(operator.itemgetter(user_class), dispatched_users.values())) >= user_count + sum(map(itemgetter(user_class), dispatched_users.values())) >= user_count for user_class, user_count in user_class_occurrences.items() ): dispatched_users.update(balanced_users) @@ -164,7 +178,7 @@ def users_to_dispatch_for_current_iteration( number_of_users_in_current_dispatch = 0 - for i, user_class in enumerate(itertools.cycle(user_class_occurrences.keys())): + for i, current_user_class in enumerate(itertools.cycle(sorted(user_class_occurrences.keys()))): # For large number of user classes and large number of workers, this assertion might fail. # If this happens, you can remove it or increase the threshold. Right now, the assertion # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). @@ -174,28 +188,30 @@ def users_to_dispatch_for_current_iteration( break if all( - sum(map(operator.itemgetter(user_class_), dispatched_users.values())) >= user_count - for user_class_, user_count in user_class_occurrences.items() + sum(map(itemgetter(user_class), dispatched_users.values())) >= user_count + for user_class, user_count in user_class_occurrences.items() ): break if ( - sum(map(operator.itemgetter(user_class), dispatched_users.values())) - >= user_class_occurrences[user_class] + sum(map(itemgetter(current_user_class), dispatched_users.values())) + >= user_class_occurrences[current_user_class] ): continue - if go_to_next_user_class(user_class, user_class_occurrences, dispatched_users, effective_balanced_users): + if go_to_next_user_class( + current_user_class, user_class_occurrences, dispatched_users, effective_balanced_users + ): continue - for j, worker_node_id in enumerate(itertools.cycle(effective_balanced_users.keys())): + for j, worker_node_id in enumerate(itertools.cycle(sorted(effective_balanced_users.keys()))): assert j < int( 2 * number_of_workers ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if effective_balanced_users[worker_node_id][user_class] == 0: + if effective_balanced_users[worker_node_id][current_user_class] == 0: continue - dispatched_users[worker_node_id][user_class] += 1 - effective_balanced_users[worker_node_id][user_class] -= 1 + dispatched_users[worker_node_id][current_user_class] += 1 + effective_balanced_users[worker_node_id][current_user_class] -= 1 number_of_users_in_current_dispatch += 1 break @@ -206,7 +222,7 @@ def users_to_dispatch_for_current_iteration( # we want each dispatch loop to be as short as possible to compute, but with # a large amount of workers/user classes, it can take longer to come up with the dispatch solution. # If the assertion is raised, then it could be a sign that the code needs to be optimized for the - # case that caused the assertion to be raised. + # situation that caused the assertion to be raised. assert time.perf_counter() - ts_dispatch < ( 0.5 if number_of_workers < 100 else 1 if number_of_workers < 250 else 1.5 if number_of_workers < 350 else 3 ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( @@ -214,8 +230,8 @@ def users_to_dispatch_for_current_iteration( ) return { - worker_node_id: dict(sorted(user_class_occurrences.items(), key=lambda x: x[0])) - for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=lambda x: x[0]) + worker_node_id: dict(sorted(user_class_occurrences.items(), key=itemgetter(0))) + for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=itemgetter(0)) } @@ -231,30 +247,32 @@ def go_to_next_user_class( a ramp up. """ dispatched_user_class_occurrences = { - user_class: sum(map(operator.itemgetter(user_class), dispatched_users.values())) + user_class: sum(map(itemgetter(user_class), dispatched_users.values())) for user_class in user_class_occurrences.keys() } - if all(n > 0 for n in dispatched_user_class_occurrences.values()): + + if all(user_count > 0 for user_count in dispatched_user_class_occurrences.values()): + # We're here because each user class have at least one user running. Thus, + # we need to ensure that the distribution of users corresponds to the weights. if not current_user_class_will_keep_distribution_better_than_all_other_user_classes( current_user_class, user_class_occurrences, dispatched_user_class_occurrences ): return True else: return False + else: - # Because each user class doesn't have at least one user, we use a simpler strategy - # that make sure the each user class appears once. - for user_class in filter( - functools.partial(operator.ne, current_user_class), sorted(user_class_occurrences.keys()) - ): - if sum(map(operator.itemgetter(user_class), effective_balanced_users.values())) == 0: + # Because each user class doesn't have at least one running user, we use a simpler strategy + # that make sure each user class appears once. + for user_class in filter(functools.partial(ne, current_user_class), sorted(user_class_occurrences.keys())): + if sum(map(itemgetter(user_class), effective_balanced_users.values())) == 0: # No more users of class `user_class` to dispatch continue if ( dispatched_user_class_occurrences[current_user_class] - dispatched_user_class_occurrences[user_class] >= 1 ): - # There's already enough `current_user_class` for the current dispatch. Hence, we should + # There's already enough users for `current_user_class` in the current dispatch. Hence, we should # not consider `current_user_class` and go to the next user class instead. return True return False @@ -300,8 +318,8 @@ def current_user_class_will_keep_distribution( # `actual_distance` corresponds to the distance from the ideal distribution for the current # dispatched users. `actual_distance_with_current_user_class` represents the distance # from the ideal distribution if we were to add one user of the given `current_user_class`. -# Thus, we strive to find the right user class to add a user in that will give us -# a `actual_distance_with_current_user_class` less than `actual_distance`. +# Thus, we want to find the best user class, in which to add a user, that will give us +# an `actual_distance_with_current_user_class` less than `actual_distance`. DistancesFromIdealDistribution = namedtuple( "DistancesFromIdealDistribution", "actual_distance actual_distance_with_current_user_class" ) @@ -312,7 +330,7 @@ def get_distances_from_ideal_distribution( user_class_occurrences: Dict[str, int], dispatched_user_class_occurrences: Dict[str, int], ) -> DistancesFromIdealDistribution: - user_classes = sorted(user_class_occurrences.keys()) + user_classes = list(user_class_occurrences.keys()) desired_weights = [ user_class_occurrences[user_class] / sum(user_class_occurrences.values()) for user_class in user_classes ] @@ -344,7 +362,8 @@ def number_of_users_left_to_dispatch( return sum( max( 0, - sum(x[user_class] for x in balanced_users.values()) - sum(x[user_class] for x in dispatched_users.values()), + sum(map(itemgetter(user_class), balanced_users.values())) + - sum(map(itemgetter(user_class), dispatched_users.values())), ) for user_class in user_class_occurrences.keys() ) @@ -384,14 +403,12 @@ def balance_users_among_workers( break if ( sum(balanced_users[worker_node.id].values()) == users_per_worker - and total_users - sum(map(sum, map(operator.methodcaller("values"), balanced_users.values()))) - > remainder + and total_users - sum(map(sum, map(methodcaller("values"), balanced_users.values()))) > remainder ): continue elif ( sum(balanced_users[worker_node.id].values()) == users_per_worker + 1 - and total_users - sum(map(sum, map(operator.methodcaller("values"), balanced_users.values()))) - < remainder + and total_users - sum(map(sum, map(methodcaller("values"), balanced_users.values()))) < remainder ): continue balanced_users[worker_node.id][user_class] += 1 diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 323a0a9e03..75bb4602b4 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -2298,7 +2298,6 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw worker_node.user_class_occurrences = {"User1": 0} worker_nodes_iterator = itertools.cycle(worker_nodes) - user_count = 0 while user_count < 25: next(worker_nodes_iterator).user_class_occurrences["User1"] += 1 From 1c7e30cc9954723e3e2f49a8d2875a4081937a17 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sat, 12 Jun 2021 16:01:55 -0400 Subject: [PATCH 078/139] Use `attrgetter` instead of `lambda` for efficiency --- locust/distribution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 2bc5e8c3de..0132906785 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -33,7 +33,7 @@ def weight_users( if len(user_classes) == 0: return {} - user_classes = sorted(user_classes, key=lambda u: u.__name__) + user_classes = sorted(user_classes, key=attrgetter("__name__")) user_class_occurrences = {user_class.__name__: 0 for user_class in user_classes} @@ -43,7 +43,7 @@ def weight_users( user_class.__name__: 1 for user_class in sorted( user_classes, - key=lambda user_class: user_class.weight, + key=attrgetter("weight"), reverse=True, )[:number_of_users] } From fa2eaa934a5a3bdefc787041c2a0f30d31b65747 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sat, 12 Jun 2021 17:20:38 -0400 Subject: [PATCH 079/139] Add comments and refactor to make code clearer (hopefully) --- locust/dispatch.py | 54 +++++++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 29d14284d7..af8084b9cc 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -101,8 +101,8 @@ def dispatch_users( # The amount of users in each user class # is less than the desired amount less_users_than_desired = all( - sum(x[user_class] for x in dispatched_users.values()) - < sum(x[user_class] for x in effective_balanced_users.values()) + number_of_dispatched_users_for_user_class(dispatched_users, user_class) + < sum(map(itemgetter(user_class), effective_balanced_users.values())) for user_class in user_class_occurrences.keys() ) @@ -160,9 +160,12 @@ def users_to_dispatch_for_current_iteration( number_of_users_per_dispatch: int, ) -> Dict[str, Dict[str, int]]: if all( - sum(map(itemgetter(user_class), dispatched_users.values())) >= user_count + number_of_dispatched_users_for_user_class(dispatched_users, user_class) >= user_count for user_class, user_count in user_class_occurrences.items() ): + # User count for every user class is greater than or equal to the target user count of each class. + # This means that we're at the last iteration of this dispatch cycle. If some user classes are in + # excess, this last iteration will stop those excess users. dispatched_users.update(balanced_users) effective_balanced_users.update( { @@ -184,17 +187,17 @@ def users_to_dispatch_for_current_iteration( # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) - if sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0: + if all_users_have_been_dispatched(effective_balanced_users): break if all( - sum(map(itemgetter(user_class), dispatched_users.values())) >= user_count + number_of_dispatched_users_for_user_class(dispatched_users, user_class) >= user_count for user_class, user_count in user_class_occurrences.items() ): break if ( - sum(map(itemgetter(current_user_class), dispatched_users.values())) + number_of_dispatched_users_for_user_class(dispatched_users, current_user_class) >= user_class_occurrences[current_user_class] ): continue @@ -235,6 +238,10 @@ def users_to_dispatch_for_current_iteration( } +def number_of_dispatched_users_for_user_class(dispatched_users: Dict[str, Dict[str, int]], user_class: str) -> int: + return sum(map(itemgetter(user_class), dispatched_users.values())) + + def go_to_next_user_class( current_user_class: str, user_class_occurrences: Dict[str, int], @@ -370,11 +377,7 @@ def number_of_users_left_to_dispatch( def all_users_have_been_dispatched(effective_balanced_users: Dict[str, Dict[str, int]]) -> bool: - return all( - user_count == 0 - for user_class_occurrences in effective_balanced_users.values() - for user_count in user_class_occurrences.values() - ) + return sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0 def balance_users_among_workers( @@ -390,28 +393,49 @@ def balance_users_among_workers( for worker_node in worker_nodes } + # We need to copy to prevent modifying `user_class_occurrences` for the parent scopes. user_class_occurrences = user_class_occurrences.copy() total_users = sum(user_class_occurrences.values()) + + # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. users_per_worker, remainder = divmod(total_users, len(worker_nodes)) for user_class in sorted(user_class_occurrences.keys()): if sum(user_class_occurrences.values()) == 0: + # No more users of any user class to assign to workers, so we can exit this loop. break + + # Assign users of `user_class` to the workers in a round-robin fashion. for worker_node in itertools.cycle(worker_nodes): if user_class_occurrences[user_class] == 0: break + + number_of_users_left_to_assign = total_users - number_of_assigned_users_across_workers(balanced_users) + if ( - sum(balanced_users[worker_node.id].values()) == users_per_worker - and total_users - sum(map(sum, map(methodcaller("values"), balanced_users.values()))) > remainder + number_of_assigned_users_for_worker(balanced_users, worker_node) == users_per_worker + and number_of_users_left_to_assign > remainder ): continue + elif ( - sum(balanced_users[worker_node.id].values()) == users_per_worker + 1 - and total_users - sum(map(sum, map(methodcaller("values"), balanced_users.values()))) < remainder + number_of_assigned_users_for_worker(balanced_users, worker_node) == users_per_worker + 1 + and number_of_users_left_to_assign < remainder ): continue + balanced_users[worker_node.id][user_class] += 1 user_class_occurrences[user_class] -= 1 return balanced_users + + +def number_of_assigned_users_for_worker( + balanced_users: Dict[str, Dict[str, int]], worker_node # type: WorkerNode +) -> int: + return sum(balanced_users[worker_node.id].values()) + + +def number_of_assigned_users_across_workers(balanced_users: Dict[str, Dict[str, int]]) -> int: + return sum(map(sum, map(methodcaller("values"), balanced_users.values()))) From 334b5b6d9dd3b29895ca2725961ab470e92e132d Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 09:14:20 -0400 Subject: [PATCH 080/139] Rename `user_class_occurrences` to `user_classes_count` As per PR review. --- locust/dispatch.py | 120 ++++++++--------- locust/distribution.py | 50 +++---- locust/runners.py | 123 +++++++++-------- locust/test/test_dispatch.py | 222 +++++++++++++++---------------- locust/test/test_distribution.py | 112 ++++++++-------- locust/test/test_env.py | 2 +- locust/test/test_runners.py | 204 ++++++++++++++-------------- 7 files changed, 412 insertions(+), 421 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index af8084b9cc..c982095ff3 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -24,12 +24,12 @@ def dispatch_users( worker_nodes, # type: List[WorkerNode] - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], spawn_rate: float, ) -> Generator[Dict[str, Dict[str, int]], None, None]: """ Generator function that dispatches the users - contained `user_class_occurrences` to the workers. + contained `user_classes_count` to the workers. The users already running on the workers are also taken into account. @@ -38,7 +38,7 @@ def dispatch_users( local or distributed mode. The spawn rate is only applied when additional users are needed. - Hence, if `user_class_occurrences` contains less users than the ones running right now, + Hence, if `user_classes_count` contains less users than the ones running right now, the dispatcher won't wait and will only run for one iteration. The rationale for not stopping users at a rate of `spawn_rate` is that stopping them is a blocking operation, especially when @@ -47,7 +47,7 @@ def dispatch_users( stop timeout is not reached yey). :param worker_nodes: List of worker nodes - :param user_class_occurrences: Desired number of users for each class + :param user_classes_count: Desired number of users for each class :param spawn_rate: The spawn rate """ # NOTE: We use "sorted" in some places in this module. It is done to ensure repeatable behaviour. @@ -60,8 +60,7 @@ def dispatch_users( # This represents the already running users among the workers initial_dispatched_users = { worker_node.id: { - user_class: worker_node.user_class_occurrences.get(user_class, 0) - for user_class in user_class_occurrences.keys() + user_class: worker_node.user_classes_count.get(user_class, 0) for user_class in user_classes_count.keys() } for worker_node in worker_nodes } @@ -69,7 +68,7 @@ def dispatch_users( # This represents the desired users distribution among the workers balanced_users = balance_users_among_workers( worker_nodes, - user_class_occurrences, + user_classes_count, ) # This represents the desired users distribution minus the already running users among the workers. @@ -83,7 +82,7 @@ def dispatch_users( 0, balanced_users[worker_node.id][user_class] - initial_dispatched_users[worker_node.id][user_class], ) - for user_class in user_class_occurrences.keys() + for user_class in user_classes_count.keys() } for worker_node in worker_nodes } @@ -103,13 +102,13 @@ def dispatch_users( less_users_than_desired = all( number_of_dispatched_users_for_user_class(dispatched_users, user_class) < sum(map(itemgetter(user_class), effective_balanced_users.values())) - for user_class in user_class_occurrences.keys() + for user_class in user_classes_count.keys() ) if less_users_than_desired: while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: users_to_dispatch = users_to_dispatch_for_current_iteration( - user_class_occurrences, + user_classes_count, dispatched_users, effective_balanced_users, balanced_users, @@ -125,7 +124,7 @@ def dispatch_users( gevent.sleep(sleep_duration) elif ( - number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) <= number_of_users_per_dispatch ): yield balanced_users @@ -133,7 +132,7 @@ def dispatch_users( else: while not all_users_have_been_dispatched(effective_balanced_users): users_to_dispatch = users_to_dispatch_for_current_iteration( - user_class_occurrences, + user_classes_count, dispatched_users, effective_balanced_users, balanced_users, @@ -153,7 +152,7 @@ def dispatch_users( def users_to_dispatch_for_current_iteration( - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], balanced_users: Dict[str, Dict[str, int]], @@ -161,7 +160,7 @@ def users_to_dispatch_for_current_iteration( ) -> Dict[str, Dict[str, int]]: if all( number_of_dispatched_users_for_user_class(dispatched_users, user_class) >= user_count - for user_class, user_count in user_class_occurrences.items() + for user_class, user_count in user_classes_count.items() ): # User count for every user class is greater than or equal to the target user count of each class. # This means that we're at the last iteration of this dispatch cycle. If some user classes are in @@ -169,8 +168,8 @@ def users_to_dispatch_for_current_iteration( dispatched_users.update(balanced_users) effective_balanced_users.update( { - worker_node_id: {user_class: 0 for user_class in user_class_occurrences.keys()} - for worker_node_id, user_class_occurrences in dispatched_users.items() + worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} + for worker_node_id, user_classes_count in dispatched_users.items() } ) @@ -181,7 +180,7 @@ def users_to_dispatch_for_current_iteration( number_of_users_in_current_dispatch = 0 - for i, current_user_class in enumerate(itertools.cycle(sorted(user_class_occurrences.keys()))): + for i, current_user_class in enumerate(itertools.cycle(sorted(user_classes_count.keys()))): # For large number of user classes and large number of workers, this assertion might fail. # If this happens, you can remove it or increase the threshold. Right now, the assertion # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). @@ -192,18 +191,18 @@ def users_to_dispatch_for_current_iteration( if all( number_of_dispatched_users_for_user_class(dispatched_users, user_class) >= user_count - for user_class, user_count in user_class_occurrences.items() + for user_class, user_count in user_classes_count.items() ): break if ( number_of_dispatched_users_for_user_class(dispatched_users, current_user_class) - >= user_class_occurrences[current_user_class] + >= user_classes_count[current_user_class] ): continue if go_to_next_user_class( - current_user_class, user_class_occurrences, dispatched_users, effective_balanced_users + current_user_class, user_classes_count, dispatched_users, effective_balanced_users ): continue @@ -229,12 +228,12 @@ def users_to_dispatch_for_current_iteration( assert time.perf_counter() - ts_dispatch < ( 0.5 if number_of_workers < 100 else 1 if number_of_workers < 250 else 1.5 if number_of_workers < 350 else 3 ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( - time.perf_counter() - ts_dispatch, number_of_workers, len(user_class_occurrences) + time.perf_counter() - ts_dispatch, number_of_workers, len(user_classes_count) ) return { - worker_node_id: dict(sorted(user_class_occurrences.items(), key=itemgetter(0))) - for worker_node_id, user_class_occurrences in sorted(dispatched_users.items(), key=itemgetter(0)) + worker_node_id: dict(sorted(user_classes_count.items(), key=itemgetter(0))) + for worker_node_id, user_classes_count in sorted(dispatched_users.items(), key=itemgetter(0)) } @@ -244,7 +243,7 @@ def number_of_dispatched_users_for_user_class(dispatched_users: Dict[str, Dict[s def go_to_next_user_class( current_user_class: str, - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], dispatched_users: Dict[str, Dict[str, int]], effective_balanced_users: Dict[str, Dict[str, int]], ) -> bool: @@ -253,16 +252,16 @@ def go_to_next_user_class( the distribution of user class stays approximately balanced during a ramp up. """ - dispatched_user_class_occurrences = { + dispatched_user_classes_count = { user_class: sum(map(itemgetter(user_class), dispatched_users.values())) - for user_class in user_class_occurrences.keys() + for user_class in user_classes_count.keys() } - if all(user_count > 0 for user_count in dispatched_user_class_occurrences.values()): + if all(user_count > 0 for user_count in dispatched_user_classes_count.values()): # We're here because each user class have at least one user running. Thus, # we need to ensure that the distribution of users corresponds to the weights. if not current_user_class_will_keep_distribution_better_than_all_other_user_classes( - current_user_class, user_class_occurrences, dispatched_user_class_occurrences + current_user_class, user_classes_count, dispatched_user_classes_count ): return True else: @@ -271,14 +270,11 @@ def go_to_next_user_class( else: # Because each user class doesn't have at least one running user, we use a simpler strategy # that make sure each user class appears once. - for user_class in filter(functools.partial(ne, current_user_class), sorted(user_class_occurrences.keys())): + for user_class in filter(functools.partial(ne, current_user_class), sorted(user_classes_count.keys())): if sum(map(itemgetter(user_class), effective_balanced_users.values())) == 0: # No more users of class `user_class` to dispatch continue - if ( - dispatched_user_class_occurrences[current_user_class] - dispatched_user_class_occurrences[user_class] - >= 1 - ): + if dispatched_user_classes_count[current_user_class] - dispatched_user_classes_count[user_class] >= 1: # There's already enough users for `current_user_class` in the current dispatch. Hence, we should # not consider `current_user_class` and go to the next user class instead. return True @@ -287,17 +283,15 @@ def go_to_next_user_class( def current_user_class_will_keep_distribution_better_than_all_other_user_classes( current_user_class: str, - user_class_occurrences: Dict[str, int], - dispatched_user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], + dispatched_user_classes_count: Dict[str, int], ) -> bool: distances = get_distances_from_ideal_distribution( - current_user_class, user_class_occurrences, dispatched_user_class_occurrences + current_user_class, user_classes_count, dispatched_user_classes_count ) if distances.actual_distance_with_current_user_class > distances.actual_distance and all( - not current_user_class_will_keep_distribution( - user_class, user_class_occurrences, dispatched_user_class_occurrences - ) - for user_class in user_class_occurrences.keys() + not current_user_class_will_keep_distribution(user_class, user_classes_count, dispatched_user_classes_count) + for user_class in user_classes_count.keys() if user_class != current_user_class ): # If we are here, it means that if one user of `current_user_class` is added @@ -311,11 +305,11 @@ def current_user_class_will_keep_distribution_better_than_all_other_user_classes def current_user_class_will_keep_distribution( current_user_class: str, - user_class_occurrences: Dict[str, int], - dispatched_user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], + dispatched_user_classes_count: Dict[str, int], ) -> bool: distances = get_distances_from_ideal_distribution( - current_user_class, user_class_occurrences, dispatched_user_class_occurrences + current_user_class, user_classes_count, dispatched_user_classes_count ) if distances.actual_distance_with_current_user_class <= distances.actual_distance: return True @@ -334,24 +328,22 @@ def current_user_class_will_keep_distribution( def get_distances_from_ideal_distribution( current_user_class: str, - user_class_occurrences: Dict[str, int], - dispatched_user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], + dispatched_user_classes_count: Dict[str, int], ) -> DistancesFromIdealDistribution: - user_classes = list(user_class_occurrences.keys()) - desired_weights = [ - user_class_occurrences[user_class] / sum(user_class_occurrences.values()) for user_class in user_classes - ] + user_classes = list(user_classes_count.keys()) + desired_weights = [user_classes_count[user_class] / sum(user_classes_count.values()) for user_class in user_classes] actual_weights = [ - dispatched_user_class_occurrences[user_class] / sum(dispatched_user_class_occurrences.values()) + dispatched_user_classes_count[user_class] / sum(dispatched_user_classes_count.values()) for user_class in user_classes ] actual_weights_with_current_user_class = [ ( - dispatched_user_class_occurrences[user_class] + 1 + dispatched_user_classes_count[user_class] + 1 if user_class == current_user_class - else dispatched_user_class_occurrences[user_class] + else dispatched_user_classes_count[user_class] ) - / (sum(dispatched_user_class_occurrences.values()) + 1) + / (sum(dispatched_user_classes_count.values()) + 1) for user_class in user_classes ] actual_distance = math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, desired_weights)))) @@ -364,7 +356,7 @@ def get_distances_from_ideal_distribution( def number_of_users_left_to_dispatch( dispatched_users: Dict[str, Dict[str, int]], balanced_users: Dict[str, Dict[str, int]], - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], ) -> int: return sum( max( @@ -372,7 +364,7 @@ def number_of_users_left_to_dispatch( sum(map(itemgetter(user_class), balanced_users.values())) - sum(map(itemgetter(user_class), dispatched_users.values())), ) - for user_class in user_class_occurrences.keys() + for user_class in user_classes_count.keys() ) @@ -382,33 +374,33 @@ def all_users_have_been_dispatched(effective_balanced_users: Dict[str, Dict[str, def balance_users_among_workers( worker_nodes, # type: List[WorkerNode] - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], ) -> Dict[str, Dict[str, int]]: """ Balance the users among the workers so that each worker gets around the same number of users of each user class """ balanced_users = { - worker_node.id: {user_class: 0 for user_class in sorted(user_class_occurrences.keys())} + worker_node.id: {user_class: 0 for user_class in sorted(user_classes_count.keys())} for worker_node in worker_nodes } - # We need to copy to prevent modifying `user_class_occurrences` for the parent scopes. - user_class_occurrences = user_class_occurrences.copy() + # We need to copy to prevent modifying `user_classes_count` for the parent scopes. + user_classes_count = user_classes_count.copy() - total_users = sum(user_class_occurrences.values()) + total_users = sum(user_classes_count.values()) # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. users_per_worker, remainder = divmod(total_users, len(worker_nodes)) - for user_class in sorted(user_class_occurrences.keys()): - if sum(user_class_occurrences.values()) == 0: + for user_class in sorted(user_classes_count.keys()): + if sum(user_classes_count.values()) == 0: # No more users of any user class to assign to workers, so we can exit this loop. break # Assign users of `user_class` to the workers in a round-robin fashion. for worker_node in itertools.cycle(worker_nodes): - if user_class_occurrences[user_class] == 0: + if user_classes_count[user_class] == 0: break number_of_users_left_to_assign = total_users - number_of_assigned_users_across_workers(balanced_users) @@ -426,7 +418,7 @@ def balance_users_among_workers( continue balanced_users[worker_node.id][user_class] += 1 - user_class_occurrences[user_class] -= 1 + user_classes_count[user_class] -= 1 return balanced_users diff --git a/locust/distribution.py b/locust/distribution.py index 0132906785..6e1bb560c7 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -35,10 +35,10 @@ def weight_users( user_classes = sorted(user_classes, key=attrgetter("__name__")) - user_class_occurrences = {user_class.__name__: 0 for user_class in user_classes} + user_classes_count = {user_class.__name__: 0 for user_class in user_classes} if number_of_users <= len(user_classes): - user_class_occurrences.update( + user_classes_count.update( { user_class.__name__: 1 for user_class in sorted( @@ -48,31 +48,31 @@ def weight_users( )[:number_of_users] } ) - return user_class_occurrences + return user_classes_count weights = list(map(attrgetter("weight"), user_classes)) - user_class_occurrences = { + user_classes_count = { user_class.__name__: round(relative_weight * number_of_users) or 1 for user_class, relative_weight in zip(user_classes, (weight / sum(weights) for weight in weights)) } - if sum(user_class_occurrences.values()) == number_of_users: - return user_class_occurrences + if sum(user_classes_count.values()) == number_of_users: + return user_classes_count else: - user_class_occurrences = _find_ideal_users_to_add_or_remove( + user_classes_count = _find_ideal_users_to_add_or_remove( user_classes, - number_of_users - sum(user_class_occurrences.values()), - user_class_occurrences, + number_of_users - sum(user_classes_count.values()), + user_classes_count, ) - assert sum(user_class_occurrences.values()) == number_of_users - return user_class_occurrences + assert sum(user_classes_count.values()) == number_of_users + return user_classes_count def _find_ideal_users_to_add_or_remove( user_classes: List[Type[User]], number_of_users_to_add_or_remove: int, - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], ) -> Dict[str, int]: sign = -1 if number_of_users_to_add_or_remove < 0 else 1 @@ -92,34 +92,34 @@ def _find_ideal_users_to_add_or_remove( max_number_of_combinations_threshold = 1000 if number_of_combinations <= max_number_of_combinations_threshold: - user_class_occurrences_candidates: Dict[float, Dict[str, int]] = {} + user_classes_count_candidates: Dict[float, Dict[str, int]] = {} for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add_or_remove): - user_class_occurrences_candidate = user_class_occurrences.copy() + user_classes_count_candidate = user_classes_count.copy() for user_class in user_classes_combination: - user_class_occurrences_candidate[user_class.__name__] += sign + user_classes_count_candidate[user_class.__name__] += sign distance = distance_from_desired_distribution( user_classes, - user_class_occurrences_candidate, + user_classes_count_candidate, ) - if distance not in user_class_occurrences_candidates: - user_class_occurrences_candidates[distance] = user_class_occurrences_candidate.copy() + if distance not in user_classes_count_candidates: + user_classes_count_candidates[distance] = user_classes_count_candidate.copy() - return user_class_occurrences_candidates[min(user_class_occurrences_candidates.keys())] + return user_classes_count_candidates[min(user_classes_count_candidates.keys())] else: - user_class_occurrences_candidate = user_class_occurrences.copy() + user_classes_count_candidate = user_classes_count.copy() for user_class in user_classes[:number_of_users_to_add_or_remove]: - user_class_occurrences_candidate[user_class.__name__] += sign - return user_class_occurrences_candidate + user_classes_count_candidate[user_class.__name__] += sign + return user_classes_count_candidate def distance_from_desired_distribution( user_classes: List[Type[User]], - user_class_occurrences: Dict[str, int], + user_classes_count: Dict[str, int], ) -> float: user_class_2_actual_ratio = { - user_class: occurrences / sum(user_class_occurrences.values()) - for user_class, occurrences in user_class_occurrences.items() + user_class: user_class_count / sum(user_classes_count.values()) + for user_class, user_class_count in user_classes_count.items() } user_class_2_expected_ratio = { diff --git a/locust/runners.py b/locust/runners.py index 39bf332c98..fe99661ec0 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -81,7 +81,7 @@ def __init__(self, environment): self.worker_cpu_warning_emitted = False self.greenlet.spawn(self.monitor_cpu).link_exception(greenlet_exception_handler) self.exceptions = {} - self.target_user_class_occurrences: Dict[str, int] = {} + self.target_user_classes_count: Dict[str, int] = {} # set up event listeners for recording requests def on_request_success(request_type, name, response_time, response_length, **_kwargs): @@ -138,11 +138,11 @@ def user_count(self): return len(self.user_greenlets) @property - def user_class_occurrences(self) -> Dict[str, int]: + def user_classes_count(self) -> Dict[str, int]: """ :returns: Number of currently running users for each user class """ - user_class_occurrences = {user_class.__name__: 0 for user_class in self.user_classes} + user_classes_count = {user_class.__name__: 0 for user_class in self.user_classes} for user_greenlet in self.user_greenlets: try: user = user_greenlet.args[0] @@ -152,8 +152,8 @@ def user_class_occurrences(self) -> Dict[str, int]: user_greenlet, ) continue - user_class_occurrences[user.__class__.__name__] += 1 - return user_class_occurrences + user_classes_count[user.__class__.__name__] += 1 + return user_classes_count def update_state(self, new_state): """ @@ -180,12 +180,12 @@ def spawn_users(self, user_classes_spawn_count: Dict[str, int], wait: bool = Fal if logger.isEnabledFor(logging.DEBUG): logger.debug( "Spawning additional %s (%s already running)..." - % (json.dumps(user_classes_spawn_count), json.dumps(self.user_class_occurrences)) + % (json.dumps(user_classes_spawn_count), json.dumps(self.user_classes_count)) ) elif sum(user_classes_spawn_count.values()) > 0: logger.info( "Spawning additional %s (%s already running)..." - % (sum(user_classes_spawn_count.values()), sum(self.user_class_occurrences.values())) + % (sum(user_classes_spawn_count.values()), sum(self.user_classes_count.values())) ) def spawn(user_class: str, spawn_count: int): @@ -210,7 +210,7 @@ def stop_users(self, user_classes_stop_count: Dict[str, int]): stop_group = Group() for user_class, stop_count in user_classes_stop_count.items(): - if self.user_class_occurrences[user_class] == 0: + if self.user_classes_count[user_class] == 0: continue to_stop = [] @@ -293,15 +293,15 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): if self.environment.host is not None: user_class.host = self.environment.host - self.target_user_class_occurrences = weight_users(self.user_classes, user_count) + self.target_user_classes_count = weight_users(self.user_classes, user_count) # Dummy worker node since dispatch_users needs it dummy_worker_node = WorkerNode(id="dummy") - dummy_worker_node.user_class_occurrences = self.user_class_occurrences + dummy_worker_node.user_classes_count = self.user_classes_count users_dispatcher = dispatch_users( worker_nodes=[dummy_worker_node], - user_class_occurrences=self.target_user_class_occurrences, + user_classes_count=self.target_user_classes_count, spawn_rate=spawn_rate, ) @@ -315,16 +315,17 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): for dispatched_users in users_dispatcher: user_classes_spawn_count = {} user_classes_stop_count = {} - user_class_occurrences = dispatched_users[dummy_worker_node.id] - logger.info("Updating running test with %d users" % (sum(user_class_occurrences.values()),)) - for user_class, occurrences in user_class_occurrences.items(): + user_classes_count = dispatched_users[dummy_worker_node.id] + logger.info("Updating running test with %d users" % (sum(user_classes_count.values()),)) + for user_class, user_class_count in user_classes_count.items(): logger.debug( - "Updating running test with %d users of class %s and wait=%r" % (occurrences, user_class, wait) + "Updating running test with %d users of class %s and wait=%r" + % (user_class_count, user_class, wait) ) - if self.user_class_occurrences[user_class] > occurrences: - user_classes_stop_count[user_class] = self.user_class_occurrences[user_class] - occurrences - elif self.user_class_occurrences[user_class] < occurrences: - user_classes_spawn_count[user_class] = occurrences - self.user_class_occurrences[user_class] + if self.user_classes_count[user_class] > user_class_count: + user_classes_stop_count[user_class] = self.user_classes_count[user_class] - user_class_count + elif self.user_classes_count[user_class] < user_class_count: + user_classes_spawn_count[user_class] = user_class_count - self.user_classes_count[user_class] if wait: # spawn_users will block, so we need to call stop_users first @@ -344,12 +345,12 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): logger.info( "All users spawned: %s (%i total running)" % ( - ", ".join("%s: %d" % (name, count) for name, count in self.user_class_occurrences.items()), - sum(self.user_class_occurrences.values()), + ", ".join("%s: %d" % (name, count) for name, count in self.user_classes_count.items()), + sum(self.user_classes_count.values()), ) ) - self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_class_occurrences.values())) + self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_classes_count.values())) def start_shape(self): if self.shape_greenlet: @@ -402,7 +403,7 @@ def stop(self): self.shape_greenlet = None self.shape_last_state = None - self.stop_users(self.user_class_occurrences) + self.stop_users(self.user_classes_count) self.update_state(STATE_STOPPED) @@ -424,7 +425,7 @@ def log_exception(self, node_id, msg, formatted_tb): @property def target_user_count(self) -> int: - return sum(self.target_user_class_occurrences.values()) + return sum(self.target_user_classes_count.values()) class LocalRunner(Runner): @@ -483,11 +484,11 @@ def __init__(self, id: str, state=STATE_INIT, heartbeat_liveness=HEARTBEAT_LIVEN self.heartbeat = heartbeat_liveness self.cpu_usage = 0 self.cpu_warning_emitted = False - self.user_class_occurrences: Dict[str, int] = {} + self.user_classes_count: Dict[str, int] = {} @property def user_count(self) -> int: - return sum(self.user_class_occurrences.values()) + return sum(self.user_classes_count.values()) class WorkerNodes(MutableMapping): @@ -578,7 +579,7 @@ def on_worker_report(client_id, data): if client_id not in self.clients: logger.info("Discarded report from unrecognized worker %s", client_id) return - self.clients[client_id].user_class_occurrences = data["user_class_occurrences"] + self.clients[client_id].user_classes_count = data["user_classes_count"] self.environment.events.worker_report.add_listener(on_worker_report) @@ -612,7 +613,7 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: if self.environment.host is not None: user_class.host = self.environment.host - self.target_user_class_occurrences = weight_users(self.user_classes, user_count) + self.target_user_classes_count = weight_users(self.user_classes, user_count) self.spawn_rate = spawn_rate @@ -638,17 +639,17 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: users_dispatcher = dispatch_users( worker_nodes=self.clients.ready + self.clients.running + self.clients.spawning, - user_class_occurrences=self.target_user_class_occurrences, + user_classes_count=self.target_user_classes_count, spawn_rate=spawn_rate, ) try: for dispatched_users in users_dispatcher: dispatch_greenlets = Group() - for worker_node_id, worker_user_class_occurrences in dispatched_users.items(): + for worker_node_id, worker_user_classes_count in dispatched_users.items(): data = { "timestamp": time.time(), - "user_class_occurrences": worker_user_class_occurrences, + "user_classes_count": worker_user_classes_count, "host": self.environment.host, "stop_timeout": self.environment.stop_timeout, } @@ -665,10 +666,8 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: logger.debug( "Currently spawned users: %s (%i total running)" % ( - ", ".join( - "%s: %d" % (name, count) for name, count in self.reported_user_class_occurrences.items() - ), - sum(self.reported_user_class_occurrences.values()), + ", ".join("%s: %d" % (name, count) for name, count in self.reported_user_classes_count.items()), + sum(self.reported_user_classes_count.values()), ) ) @@ -693,8 +692,8 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: logger.info( "All users spawned: %s (%i total running)" % ( - ", ".join("%s: %d" % (name, count) for name, count in self.reported_user_class_occurrences.items()), - sum(self.reported_user_class_occurrences.values()), + ", ".join("%s: %d" % (name, count) for name, count in self.reported_user_classes_count.items()), + sum(self.reported_user_classes_count.values()), ) ) @@ -753,7 +752,7 @@ def heartbeat_worker(self): if client.heartbeat < 0 and client.state != STATE_MISSING: logger.info("Worker %s failed to send heartbeat, setting state to missing." % str(client.id)) client.state = STATE_MISSING - client.user_class_occurrences = {} + client.user_classes_count = {} if self.worker_count <= 0: logger.info("The last worker went missing, stopping test.") self.stop() @@ -805,9 +804,9 @@ def client_listener(self): logger.info( "Worker %s self-healed with heartbeat, setting state to %s." % (str(c.id), client_state) ) - user_class_occurrences = msg.data.get("user_class_occurrences") - if user_class_occurrences: - c.user_class_occurrences = user_class_occurrences + user_classes_count = msg.data.get("user_classes_count") + if user_classes_count: + c.user_classes_count = user_classes_count c.state = client_state c.cpu_usage = msg.data["current_cpu_usage"] if not c.cpu_warning_emitted and c.cpu_usage > 90: @@ -822,10 +821,10 @@ def client_listener(self): self.clients[msg.node_id].state = STATE_SPAWNING elif msg.type == "spawning_complete": self.clients[msg.node_id].state = STATE_RUNNING - self.clients[msg.node_id].user_class_occurrences = msg.data["user_class_occurrences"] + self.clients[msg.node_id].user_classes_count = msg.data["user_classes_count"] if len(self.clients.spawning) == 0: self.environment.events.spawning_complete.fire( - user_count=sum(self.target_user_class_occurrences.values()) + user_count=sum(self.target_user_classes_count.values()) ) elif msg.type == "quit": if msg.node_id in self.clients: @@ -848,12 +847,12 @@ def worker_count(self): return len(self.clients.ready) + len(self.clients.spawning) + len(self.clients.running) @property - def reported_user_class_occurrences(self) -> Dict[str, int]: - reported_user_class_occurrences = defaultdict(lambda: 0) + def reported_user_classes_count(self) -> Dict[str, int]: + reported_user_classes_count = defaultdict(lambda: 0) for client in self.clients.ready + self.clients.spawning + self.clients.running: - for name, count in client.user_class_occurrences.items(): - reported_user_class_occurrences[name] += count - return reported_user_class_occurrences + for name, count in client.user_classes_count.items(): + reported_user_classes_count[name] += count + return reported_user_classes_count class WorkerRunner(DistributedRunner): @@ -885,11 +884,11 @@ def __init__(self, environment, master_host, master_port): # register listener for when all users have spawned, and report it to the master node def on_spawning_complete(user_count): - assert user_count == sum(self.user_class_occurrences.values()) + assert user_count == sum(self.user_classes_count.values()) self.client.send( Message( "spawning_complete", - {"user_class_occurrences": self.user_class_occurrences, "user_count": self.user_count}, + {"user_classes_count": self.user_classes_count, "user_count": self.user_count}, self.client_id, ) ) @@ -899,7 +898,7 @@ def on_spawning_complete(user_count): # register listener that adds the current number of spawned users to the report that is sent to the master node def on_report_to_master(client_id, data): - data["user_class_occurrences"] = self.user_class_occurrences + data["user_classes_count"] = self.user_classes_count data["user_count"] = self.user_count self.environment.events.report_to_master.add_listener(on_report_to_master) @@ -920,13 +919,13 @@ def on_user_error(user_instance, exception, tb): def start(self, user_count, spawn_rate, wait=False): raise NotImplementedError("use start_worker") - def start_worker(self, user_class_occurrences: Dict[str, int], **kwargs): + def start_worker(self, user_classes_count: Dict[str, int], **kwargs): """ Start running a load test as a worker - :param user_class_occurrences: Users to run + :param user_classes_count: Users to run """ - self.target_user_class_occurrences = user_class_occurrences + self.target_user_classes_count = user_classes_count if self.worker_state != STATE_RUNNING and self.worker_state != STATE_SPAWNING: self.stats.clear_all() @@ -937,18 +936,18 @@ def start_worker(self, user_class_occurrences: Dict[str, int], **kwargs): user_classes_spawn_count = {} user_classes_stop_count = {} - for user_class, occurrences in user_class_occurrences.items(): - if self.user_class_occurrences[user_class] > occurrences: - user_classes_stop_count[user_class] = self.user_class_occurrences[user_class] - occurrences - elif self.user_class_occurrences[user_class] < occurrences: - user_classes_spawn_count[user_class] = occurrences - self.user_class_occurrences[user_class] + for user_class, user_class_count in user_classes_count.items(): + if self.user_classes_count[user_class] > user_class_count: + user_classes_stop_count[user_class] = self.user_classes_count[user_class] - user_class_count + elif self.user_classes_count[user_class] < user_class_count: + user_classes_spawn_count[user_class] = user_class_count - self.user_classes_count[user_class] # call spawn_users before stopping the users since stop_users # can be blocking because of the stop_timeout self.spawn_users(user_classes_spawn_count) self.stop_users(user_classes_stop_count) - self.environment.events.spawning_complete.fire(user_count=sum(self.user_class_occurrences.values())) + self.environment.events.spawning_complete.fire(user_count=sum(self.user_classes_count.values())) def heartbeat(self): while True: @@ -959,7 +958,7 @@ def heartbeat(self): { "state": self.worker_state, "current_cpu_usage": self.current_cpu_usage, - "user_class_occurrences": self.user_class_occurrences, + "user_classes_count": self.user_classes_count, }, self.client_id, ) @@ -999,7 +998,7 @@ def worker(self): if self.spawning_greenlet: # kill existing spawning greenlet before we launch new one self.spawning_greenlet.kill(block=True) - self.spawning_greenlet = self.greenlet.spawn(lambda: self.start_worker(job["user_class_occurrences"])) + self.spawning_greenlet = self.greenlet.spawn(lambda: self.start_worker(job["user_classes_count"])) self.spawning_greenlet.link_exception(greenlet_exception_handler) last_received_spawn_timestamp = job["timestamp"] elif msg.type == "stop": diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 75bb4602b4..bb43034b20 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -17,31 +17,31 @@ def test_balance_users_among_1_worker(self): balanced_users = balance_users_among_workers( worker_nodes=[worker_node1], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, ) self.assertDictEqual(balanced_users, {"1": {"User1": 3, "User2": 3, "User3": 3}}) balanced_users = balance_users_among_workers( worker_nodes=[worker_node1], - user_class_occurrences={"User1": 5, "User2": 4, "User3": 2}, + user_classes_count={"User1": 5, "User2": 4, "User3": 2}, ) self.assertDictEqual(balanced_users, {"1": {"User1": 5, "User2": 4, "User3": 2}}) balanced_users = balance_users_among_workers( worker_nodes=[worker_node1], - user_class_occurrences={"User1": 1, "User2": 1, "User3": 1}, + user_classes_count={"User1": 1, "User2": 1, "User3": 1}, ) self.assertDictEqual(balanced_users, {"1": {"User1": 1, "User2": 1, "User3": 1}}) balanced_users = balance_users_among_workers( worker_nodes=[worker_node1], - user_class_occurrences={"User1": 1, "User2": 1, "User3": 0}, + user_classes_count={"User1": 1, "User2": 1, "User3": 0}, ) self.assertDictEqual(balanced_users, {"1": {"User1": 1, "User2": 1, "User3": 0}}) balanced_users = balance_users_among_workers( worker_nodes=[worker_node1], - user_class_occurrences={"User1": 0, "User2": 0, "User3": 0}, + user_classes_count={"User1": 0, "User2": 0, "User3": 0}, ) self.assertDictEqual(balanced_users, {"1": {"User1": 0, "User2": 0, "User3": 0}}) @@ -52,7 +52,7 @@ def test_balance_users_among_3_workers(self): balanced_users = balance_users_among_workers( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, ) expected_balanced_users = { "1": {"User1": 1, "User2": 1, "User3": 1}, @@ -63,7 +63,7 @@ def test_balance_users_among_3_workers(self): balanced_users = balance_users_among_workers( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 5, "User2": 4, "User3": 2}, + user_classes_count={"User1": 5, "User2": 4, "User3": 2}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 1}, @@ -74,7 +74,7 @@ def test_balance_users_among_3_workers(self): balanced_users = balance_users_among_workers( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 1, "User2": 1, "User3": 1}, + user_classes_count={"User1": 1, "User2": 1, "User3": 1}, ) expected_balanced_users = { "1": {"User1": 1, "User2": 0, "User3": 0}, @@ -85,7 +85,7 @@ def test_balance_users_among_3_workers(self): balanced_users = balance_users_among_workers( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 1, "User2": 1, "User3": 0}, + user_classes_count={"User1": 1, "User2": 1, "User3": 0}, ) expected_balanced_users = { "1": {"User1": 1, "User2": 0, "User3": 0}, @@ -96,7 +96,7 @@ def test_balance_users_among_3_workers(self): balanced_users = balance_users_among_workers( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 0, "User2": 0, "User3": 0}, + user_classes_count={"User1": 0, "User2": 0, "User3": 0}, ) expected_balanced_users = { "1": {"User1": 0, "User2": 0, "User3": 0}, @@ -110,7 +110,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 10, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, + user_classes_count={"User1": 10, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, ) expected_balanced_users = { "1": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users @@ -128,7 +128,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, + user_classes_count={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users @@ -146,7 +146,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 6}, + user_classes_count={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 6}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users @@ -164,7 +164,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 5, "User3": 5, "User4": 6, "User5": 6}, + user_classes_count={"User1": 11, "User2": 5, "User3": 5, "User4": 6, "User5": 6}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users @@ -182,7 +182,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 5, "User3": 6, "User4": 6, "User5": 6}, + user_classes_count={"User1": 11, "User2": 5, "User3": 6, "User4": 6, "User5": 6}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users @@ -200,7 +200,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 6}, + user_classes_count={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 6}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users @@ -218,7 +218,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 7}, + user_classes_count={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 7}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users @@ -236,7 +236,7 @@ def test_balance_5_users_among_10_workers(self): balanced_users = balance_users_among_workers( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 11}, + user_classes_count={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 11}, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 4 users @@ -261,7 +261,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.15, ) @@ -387,7 +387,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.5, ) @@ -511,7 +511,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1, ) @@ -635,7 +635,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2, ) @@ -711,7 +711,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2.4, ) @@ -789,7 +789,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=3, ) @@ -841,7 +841,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=4, ) @@ -893,7 +893,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=9, ) @@ -918,15 +918,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.15, ) @@ -1023,15 +1023,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.5, ) @@ -1126,15 +1126,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1, ) @@ -1229,15 +1229,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2, ) @@ -1296,15 +1296,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2.4, ) @@ -1365,15 +1365,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=3, ) @@ -1420,15 +1420,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=4, ) @@ -1463,15 +1463,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 1} + worker_node3.user_classes_count = {"User2": 1} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=9, ) @@ -1496,15 +1496,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.15, ) @@ -1565,15 +1565,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.5, ) @@ -1632,15 +1632,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1, ) @@ -1699,15 +1699,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2, ) @@ -1754,15 +1754,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2.4, ) @@ -1811,15 +1811,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=3, ) @@ -1842,15 +1842,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=4, ) @@ -1873,15 +1873,15 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {} + worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=9, ) @@ -1906,16 +1906,16 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): def test_dispatch_users_to_3_workers(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {"User3": 15} + worker_node1.user_classes_count = {"User3": 15} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 5} + worker_node2.user_classes_count = {"User1": 5} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User2": 7} + worker_node3.user_classes_count = {"User2": 7} for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=spawn_rate, ) @@ -1940,16 +1940,16 @@ def test_dispatch_users_to_3_workers(self): class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): def test_dispatch_users_to_3_workers(self): worker_node1 = WorkerNode("1") - worker_node1.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} + worker_node1.user_classes_count = {"User1": 1, "User2": 1, "User3": 1} worker_node2 = WorkerNode("2") - worker_node2.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} + worker_node2.user_classes_count = {"User1": 1, "User2": 1, "User3": 1} worker_node3 = WorkerNode("3") - worker_node3.user_class_occurrences = {"User1": 1, "User2": 1, "User3": 1} + worker_node3.user_classes_count = {"User1": 1, "User2": 1, "User3": 1} for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], - user_class_occurrences={"User1": 3, "User2": 3, "User3": 3}, + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=spawn_rate, ) @@ -1986,7 +1986,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3, worker_node4], - user_class_occurrences={"User1": 25, "User2": 50}, + user_classes_count={"User1": 25, "User2": 50}, spawn_rate=5, ) @@ -2295,17 +2295,17 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw worker_nodes = [WorkerNode(str(i)) for i in range(1, 21)] for worker_node in worker_nodes: - worker_node.user_class_occurrences = {"User1": 0} + worker_node.user_classes_count = {"User1": 0} worker_nodes_iterator = itertools.cycle(worker_nodes) user_count = 0 while user_count < 25: - next(worker_nodes_iterator).user_class_occurrences["User1"] += 1 + next(worker_nodes_iterator).user_classes_count["User1"] += 1 user_count += 1 users_dispatcher = dispatch_users( worker_nodes=worker_nodes, - user_class_occurrences={"User1": 50}, + user_classes_count={"User1": 50}, spawn_rate=1, ) @@ -2361,24 +2361,24 @@ def test_dispatch_users_to_workers(self): worker_nodes = [] for i in range(1, number_of_workers + 1): worker_node = WorkerNode(str(i)) - worker_node.user_class_occurrences = { + worker_node.user_classes_count = { f"User{i}": next(number_of_prior_users_iterator) for i in range(1, number_of_user_classes + 1) } worker_nodes.append(worker_node) - user_class_occurrences = { + user_classes_count = { f"User{i}": next(weights_iterator) for i in range(1, number_of_user_classes + 1) } # We limit the maximum total dispatch to around 10s (i.e. total_number_of_users / 10) # so that the test does take too much time. - total_number_of_users = sum(user_class_occurrences.values()) + total_number_of_users = sum(user_classes_count.values()) spawn_rate = max(total_number_of_users / 10, 100 * next(spawn_rate_multipliers_iterator)) users_dispatcher = dispatch_users( worker_nodes=worker_nodes, - user_class_occurrences=user_class_occurrences, + user_classes_count=user_classes_count, spawn_rate=spawn_rate, ) @@ -2407,7 +2407,7 @@ def test_dispatch_users_to_workers(self): class TestNumberOfUsersLeftToDispatch(unittest.TestCase): def test_number_of_users_left_to_dispatch(self): - user_class_occurrences = {"User1": 6, "User2": 2, "User3": 8} + user_classes_count = {"User1": 6, "User2": 2, "User3": 8} balanced_users = { "Worker1": {"User1": 3, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, @@ -2417,28 +2417,28 @@ def test_number_of_users_left_to_dispatch(self): "Worker1": {"User1": 5, "User2": 2, "User3": 6}, "Worker2": {"User1": 5, "User2": 2, "User3": 6}, } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) self.assertEqual(0, result) dispatched_users = { "Worker1": {"User1": 2, "User2": 0, "User3": 4}, "Worker2": {"User1": 2, "User2": 0, "User3": 4}, } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) self.assertEqual(4, result) dispatched_users = { "Worker1": {"User1": 3, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 0, "User3": 4}, } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) self.assertEqual(1, result) dispatched_users = { "Worker1": {"User1": 3, "User2": 1, "User3": 4}, "Worker2": {"User1": 3, "User2": 1, "User3": 4}, } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_class_occurrences) + result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) self.assertEqual(0, result) diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 79fe0e69d9..c73fa10219 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -7,17 +7,17 @@ class TestDistribution(unittest.TestCase): def test_distribution_no_user_classes(self): - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[], number_of_users=0, ) - self.assertDictEqual(user_class_occurrences, {}) + self.assertDictEqual(user_classes_count, {}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[], number_of_users=1, ) - self.assertDictEqual(user_class_occurrences, {}) + self.assertDictEqual(user_classes_count, {}) def test_distribution_equal_weights_and_fewer_amount_than_user_classes(self): class User1(User): @@ -29,23 +29,23 @@ class User2(User): class User3(User): weight = 1 - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=0, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 0}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=1, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 0, "User3": 0}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 0, "User3": 0}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=2, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 0}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 0}) def test_distribution_equal_weights(self): class User1(User): @@ -57,29 +57,29 @@ class User2(User): class User3(User): weight = 1 - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=3, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=4, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 1, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 1, "User3": 1}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=5, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=6, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 2, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 2, "User3": 2}) def test_distribution_unequal_and_unique_weights_and_fewer_amount_than_user_classes(self): class User1(User): @@ -91,23 +91,23 @@ class User2(User): class User3(User): weight = 3 - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=0, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 0}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=1, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 1}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=2, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 1, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 1}) def test_distribution_unequal_and_unique_weights(self): class User1(User): @@ -119,41 +119,41 @@ class User2(User): class User3(User): weight = 3 - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=3, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=4, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 2}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=5, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=6, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 3}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 3}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=10, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 3, "User3": 5}) + self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 3, "User3": 5}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=11, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 4, "User3": 5}) + self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 4, "User3": 5}) def test_distribution_unequal_and_non_unique_weights_and_fewer_amount_than_user_classes(self): class User1(User): @@ -165,23 +165,23 @@ class User2(User): class User3(User): weight = 2 - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=0, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 0, "User3": 0}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=1, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 1, "User3": 0}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 0}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=2, ) - self.assertDictEqual(user_class_occurrences, {"User1": 0, "User2": 1, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 1}) def test_distribution_unequal_and_non_unique_weights(self): class User1(User): @@ -193,41 +193,41 @@ class User2(User): class User3(User): weight = 2 - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=3, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 1}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=4, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 1, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 2}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=5, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 2, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=6, ) - self.assertDictEqual(user_class_occurrences, {"User1": 1, "User2": 3, "User3": 2}) + self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 3, "User3": 2}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=10, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 4, "User3": 4}) + self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 4, "User3": 4}) - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[User1, User2, User3], number_of_users=11, ) - self.assertDictEqual(user_class_occurrences, {"User1": 2, "User2": 5, "User3": 4}) + self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 5, "User3": 4}) def test_distribution_large_number_of_users(self): class User1(User): @@ -277,7 +277,7 @@ class User15(User): for number_of_users in range(1044523783783, 1044523783783 + 1000): ts = time.perf_counter() - user_class_occurrences = weight_users( + user_classes_count = weight_users( user_classes=[ User1, User2, @@ -298,5 +298,5 @@ class User15(User): number_of_users=number_of_users, ) delta_ms = 1e3 * (time.perf_counter() - ts) - self.assertEqual(sum(user_class_occurrences.values()), number_of_users) + self.assertEqual(sum(user_classes_count.values()), number_of_users) self.assertLessEqual(delta_ms, 100) diff --git a/locust/test/test_env.py b/locust/test/test_env.py index 6fa23d8155..b6a4afb61d 100644 --- a/locust/test/test_env.py +++ b/locust/test/test_env.py @@ -10,7 +10,7 @@ class TestEnvironment(LocustTestCase): - def test_user_class_occurrences(self): + def test_user_classes_count(self): class MyUser1(User): wait_time = constant(0) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index d115384697..507307ac61 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -430,15 +430,15 @@ def my_task(self): runner.start(user_count=10, spawn_rate=5, wait=False) runner.spawning_greenlet.join() - self.assertDictEqual({"MyUser1": 5, "MyUser2": 5}, runner.user_class_occurrences) + self.assertDictEqual({"MyUser1": 5, "MyUser2": 5}, runner.user_classes_count) runner.start(user_count=5, spawn_rate=5, wait=False) runner.spawning_greenlet.join() - self.assertDictEqual({"MyUser1": 3, "MyUser2": 2}, runner.user_class_occurrences) + self.assertDictEqual({"MyUser1": 3, "MyUser2": 2}, runner.user_classes_count) runner.quit() - def test_user_class_occurrences(self): + def test_user_classes_count(self): class MyUser1(User): wait_time = constant(0) @@ -458,11 +458,11 @@ def my_task(self): runner.start(user_count=10, spawn_rate=5, wait=False) runner.spawning_greenlet.join() - self.assertDictEqual({"MyUser1": 5, "MyUser2": 5}, runner.user_class_occurrences) + self.assertDictEqual({"MyUser1": 5, "MyUser2": 5}, runner.user_classes_count) runner.start(user_count=5, spawn_rate=5, wait=False) runner.spawning_greenlet.join() - self.assertDictEqual({"MyUser1": 3, "MyUser2": 2}, runner.user_class_occurrences) + self.assertDictEqual({"MyUser1": 3, "MyUser2": 2}, runner.user_classes_count) runner.quit() @@ -625,7 +625,7 @@ def tick(self): self.assertEqual( 9, test_shape.get_current_user_count(), "Shape is not seeing stage 1 runner user count correctly" ) - self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 9}) + self.assertDictEqual(master.reported_user_classes_count, {"TestUser": 9}) # Ensure new stage with more users has been reached sleep(2) @@ -634,7 +634,7 @@ def tick(self): self.assertEqual( 21, test_shape.get_current_user_count(), "Shape is not seeing stage 2 runner user count correctly" ) - self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 21}) + self.assertDictEqual(master.reported_user_classes_count, {"TestUser": 21}) # Ensure new stage with less users has been reached sleep(2) @@ -643,7 +643,7 @@ def tick(self): self.assertEqual( 3, test_shape.get_current_user_count(), "Shape is not seeing stage 3 runner user count correctly" ) - self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 3}) + self.assertDictEqual(master.reported_user_classes_count, {"TestUser": 3}) # Ensure test stops at the end sleep(2) @@ -652,7 +652,7 @@ def tick(self): self.assertEqual( 0, test_shape.get_current_user_count(), "Shape is not seeing stopped runner user count correctly" ) - self.assertDictEqual(master.reported_user_class_occurrences, {"TestUser": 0}) + self.assertDictEqual(master.reported_user_classes_count, {"TestUser": 0}) self.assertEqual("stopped", master.state) @@ -746,16 +746,16 @@ def tick(self): w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + self.assertDictEqual(w1, workers[0].user_classes_count) + self.assertDictEqual(w2, workers[1].user_classes_count) + self.assertDictEqual(w3, workers[2].user_classes_count) + self.assertDictEqual(w4, workers[3].user_classes_count) + self.assertDictEqual(w5, workers[4].user_classes_count) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_classes_count) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_classes_count) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) sleep(5) # runtime = 10s # Second stage @@ -770,16 +770,16 @@ def tick(self): w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 0} w4 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 1} w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 2} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + self.assertDictEqual(w1, workers[0].user_classes_count) + self.assertDictEqual(w2, workers[1].user_classes_count) + self.assertDictEqual(w3, workers[2].user_classes_count) + self.assertDictEqual(w4, workers[3].user_classes_count) + self.assertDictEqual(w5, workers[4].user_classes_count) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_classes_count) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_classes_count) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) sleep(5) # runtime = 20s # Third stage @@ -798,16 +798,16 @@ def tick(self): w3 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w4 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w5 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + self.assertDictEqual(w1, workers[0].user_classes_count) + self.assertDictEqual(w2, workers[1].user_classes_count) + self.assertDictEqual(w3, workers[2].user_classes_count) + self.assertDictEqual(w4, workers[3].user_classes_count) + self.assertDictEqual(w5, workers[4].user_classes_count) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_classes_count) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_classes_count) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) sleep(10 - (time.time() - ts)) # runtime = 40s # Fourth stage @@ -826,16 +826,16 @@ def tick(self): w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} w4 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} w5 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + self.assertDictEqual(w1, workers[0].user_classes_count) + self.assertDictEqual(w2, workers[1].user_classes_count) + self.assertDictEqual(w3, workers[2].user_classes_count) + self.assertDictEqual(w4, workers[3].user_classes_count) + self.assertDictEqual(w5, workers[4].user_classes_count) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_classes_count) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_classes_count) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) sleep(1) # runtime = 46s # Fourth stage - All users are now at the desired number @@ -849,16 +849,16 @@ def tick(self): w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 0} w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 1} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + self.assertDictEqual(w1, workers[0].user_classes_count) + self.assertDictEqual(w2, workers[1].user_classes_count) + self.assertDictEqual(w3, workers[2].user_classes_count) + self.assertDictEqual(w4, workers[3].user_classes_count) + self.assertDictEqual(w5, workers[4].user_classes_count) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_classes_count) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_classes_count) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) sleep(10 - delta) # runtime = 56s # Sleep stop_timeout and make sure the test has stopped @@ -886,16 +886,16 @@ def tick(self): w3 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w4 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} w5 = {"TestUser1": 0, "TestUser2": 0, "TestUser3": 0} - self.assertDictEqual(w1, workers[0].user_class_occurrences) - self.assertDictEqual(w2, workers[1].user_class_occurrences) - self.assertDictEqual(w3, workers[2].user_class_occurrences) - self.assertDictEqual(w4, workers[3].user_class_occurrences) - self.assertDictEqual(w5, workers[4].user_class_occurrences) - self.assertDictEqual(w1, master.clients[workers[0].client_id].user_class_occurrences) - self.assertDictEqual(w2, master.clients[workers[1].client_id].user_class_occurrences) - self.assertDictEqual(w3, master.clients[workers[2].client_id].user_class_occurrences) - self.assertDictEqual(w4, master.clients[workers[3].client_id].user_class_occurrences) - self.assertDictEqual(w5, master.clients[workers[4].client_id].user_class_occurrences) + self.assertDictEqual(w1, workers[0].user_classes_count) + self.assertDictEqual(w2, workers[1].user_classes_count) + self.assertDictEqual(w3, workers[2].user_classes_count) + self.assertDictEqual(w4, workers[3].user_classes_count) + self.assertDictEqual(w5, workers[4].user_classes_count) + self.assertDictEqual(w1, master.clients[workers[0].client_id].user_classes_count) + self.assertDictEqual(w2, master.clients[workers[1].client_id].user_classes_count) + self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) + self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) + self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) ts = time.time() while master.state != STATE_STOPPED: @@ -1269,12 +1269,12 @@ def my_task(self): master.start(100, 20) self.assertEqual(5, len(server.outbox)) for i, (_, msg) in enumerate(server.outbox.copy()): - self.assertDictEqual({"TestUser": int((i + 1) * 20)}, msg.data["user_class_occurrences"]) + self.assertDictEqual({"TestUser": int((i + 1) * 20)}, msg.data["user_classes_count"]) server.outbox.pop() # Normally, this attribute would be updated when the # master receives the report from the worker. - master.clients["zeh_fake_client1"].user_class_occurrences = {"TestUser": 100} + master.clients["zeh_fake_client1"].user_classes_count = {"TestUser": 100} # let another worker connect server.mocked_send(Message("client_ready", None, "zeh_fake_client2")) @@ -1282,9 +1282,9 @@ def my_task(self): sleep(0.1) # give time for messages to be sent to clients self.assertEqual(2, len(server.outbox)) client_id, msg = server.outbox.pop() - self.assertEqual({"TestUser": 50}, msg.data["user_class_occurrences"]) + self.assertEqual({"TestUser": 50}, msg.data["user_classes_count"]) client_id, msg = server.outbox.pop() - self.assertEqual({"TestUser": 50}, msg.data["user_class_occurrences"]) + self.assertEqual({"TestUser": 50}, msg.data["user_classes_count"]) def test_sends_spawn_data_to_ready_running_spawning_workers(self): """Sends spawn job to running, ready, or spawning workers""" @@ -1449,7 +1449,7 @@ def my_task(self): master.start(7, 7) self.assertEqual(5, len(server.outbox)) - num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) + num_users = sum(sum(msg.data["user_classes_count"].values()) for _, msg in server.outbox if msg.data) self.assertEqual(7, num_users, "Total number of locusts that would have been spawned is not 7") @@ -1468,7 +1468,7 @@ def my_task(self): master.start(2, 2) self.assertEqual(5, len(server.outbox)) - num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) + num_users = sum(sum(msg.data["user_classes_count"].values()) for _, msg in server.outbox if msg.data) self.assertEqual(2, num_users, "Total number of locusts that would have been spawned is not 2") @@ -1501,14 +1501,14 @@ def tick(self): sleep(0.5) # Wait for shape_worker to update user_count - num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) + num_users = sum(sum(msg.data["user_classes_count"].values()) for _, msg in server.outbox if msg.data) self.assertEqual( 1, num_users, "Total number of users in first stage of shape test is not 1: %i" % num_users ) # Wait for shape_worker to update user_count again sleep(2) - num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) + num_users = sum(sum(msg.data["user_classes_count"].values()) for _, msg in server.outbox if msg.data) self.assertEqual( 3, num_users, "Total number of users in second stage of shape test is not 3: %i" % num_users ) @@ -1546,7 +1546,7 @@ def tick(self): sleep(0.5) # Wait for shape_worker to update user_count - num_users = sum(sum(msg.data["user_class_occurrences"].values()) for _, msg in server.outbox if msg.data) + num_users = sum(sum(msg.data["user_classes_count"].values()) for _, msg in server.outbox if msg.data) self.assertEqual( 5, num_users, "Total number of users in first stage of shape test is not 5: %i" % num_users ) @@ -1557,7 +1557,7 @@ def tick(self): for _, msg in server.outbox: if not msg.data: continue - msgs[msg.node_id][msg.data["timestamp"]] = sum(msg.data["user_class_occurrences"].values()) + msgs[msg.node_id][msg.data["timestamp"]] = sum(msg.data["user_classes_count"].values()) # Count users for the last received messages num_users = sum(v[max(v.keys())] for v in msgs.values()) self.assertEqual( @@ -1658,17 +1658,17 @@ def my_task(self): server.mocked_send(Message("client_ready", None, "fake_client1")) master.start(7, 7) - self.assertEqual({"MyUser1": 3, "MyUser2": 4}, master.target_user_class_occurrences) + self.assertEqual({"MyUser1": 3, "MyUser2": 4}, master.target_user_classes_count) self.assertEqual(7, master.target_user_count) self.assertEqual(7, master.spawn_rate) master.start(10, 10) - self.assertEqual({"MyUser1": 5, "MyUser2": 5}, master.target_user_class_occurrences) + self.assertEqual({"MyUser1": 5, "MyUser2": 5}, master.target_user_classes_count) self.assertEqual(10, master.target_user_count) self.assertEqual(10, master.spawn_rate) master.start(1, 3) - self.assertEqual({"MyUser1": 1, "MyUser2": 0}, master.target_user_class_occurrences) + self.assertEqual({"MyUser1": 1, "MyUser2": 0}, master.target_user_classes_count) self.assertEqual(1, master.target_user_count) self.assertEqual(3, master.spawn_rate) @@ -1728,7 +1728,7 @@ def on_test_start(_environment, **kw): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyTestUser": 1}, + "user_classes_count": {"MyTestUser": 1}, "host": "", "stop_timeout": 1, }, @@ -1770,7 +1770,7 @@ def the_task(self): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyTestUser": 1}, + "user_classes_count": {"MyTestUser": 1}, "host": "", "stop_timeout": None, }, @@ -1814,7 +1814,7 @@ def my_task(self): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyUser": 10}, + "user_classes_count": {"MyUser": 10}, "host": "", "stop_timeout": None, }, @@ -1832,7 +1832,7 @@ def my_task(self): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyUser": 9}, + "user_classes_count": {"MyUser": 9}, "host": "", "stop_timeout": None, }, @@ -1849,7 +1849,7 @@ def my_task(self): "spawn", { "timestamp": 1605538583, - "user_class_occurrences": {"MyUser": 2}, + "user_classes_count": {"MyUser": 2}, "host": "", "stop_timeout": None, }, @@ -1866,7 +1866,7 @@ def my_task(self): "spawn", { "timestamp": 1605538585, - "user_class_occurrences": {"MyUser": 2}, + "user_classes_count": {"MyUser": 2}, "host": "", "stop_timeout": None, }, @@ -1880,7 +1880,7 @@ def my_task(self): def test_worker_messages_sent_to_master(self): """ - Ensure that worker includes both "user_count" and "user_class_occurrences" + Ensure that worker includes both "user_count" and "user_classes_count" when reporting to the master. """ @@ -1906,7 +1906,7 @@ def my_task(self): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyUser": 10}, + "user_classes_count": {"MyUser": 10}, "host": "", "stop_timeout": None, }, @@ -1923,16 +1923,16 @@ def my_task(self): message = next((m for m in reversed(client.outbox) if m.type == "stats"), None) self.assertIsNotNone(message) self.assertIn("user_count", message.data) - self.assertIn("user_class_occurrences", message.data) + self.assertIn("user_classes_count", message.data) self.assertEqual(message.data["user_count"], 10) - self.assertEqual(message.data["user_class_occurrences"]["MyUser"], 10) + self.assertEqual(message.data["user_classes_count"]["MyUser"], 10) message = next((m for m in client.outbox if m.type == "spawning_complete"), None) self.assertIsNotNone(message) self.assertIn("user_count", message.data) - self.assertIn("user_class_occurrences", message.data) + self.assertIn("user_classes_count", message.data) self.assertEqual(message.data["user_count"], 10) - self.assertEqual(message.data["user_class_occurrences"]["MyUser"], 10) + self.assertEqual(message.data["user_classes_count"]["MyUser"], 10) worker.quit() @@ -1959,7 +1959,7 @@ def my_task(self): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyUser": 10}, + "user_classes_count": {"MyUser": 10}, "host": "", "stop_timeout": None, }, @@ -1973,7 +1973,7 @@ def my_task(self): "spawn", { "timestamp": 1605538585, - "user_class_occurrences": {"MyUser": 9}, + "user_classes_count": {"MyUser": 9}, "host": "", "stop_timeout": None, }, @@ -2009,7 +2009,7 @@ def my_task(self): "spawn", { "timestamp": 1605538584, - "user_class_occurrences": {"MyUser1": 10, "MyUser2": 10}, + "user_classes_count": {"MyUser1": 10, "MyUser2": 10}, "host": "", "stop_timeout": None, }, @@ -2017,8 +2017,8 @@ def my_task(self): ) ) worker.spawning_greenlet.join() - self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 10, "MyUser2": 10}) - self.assertDictEqual(worker.target_user_class_occurrences, {"MyUser1": 10, "MyUser2": 10}) + self.assertDictEqual(worker.user_classes_count, {"MyUser1": 10, "MyUser2": 10}) + self.assertDictEqual(worker.target_user_classes_count, {"MyUser1": 10, "MyUser2": 10}) self.assertEqual(worker.target_user_count, 20) client.mocked_send( @@ -2026,7 +2026,7 @@ def my_task(self): "spawn", { "timestamp": 1605538585, - "user_class_occurrences": {"MyUser1": 1, "MyUser2": 2}, + "user_classes_count": {"MyUser1": 1, "MyUser2": 2}, "host": "", "stop_timeout": None, }, @@ -2034,8 +2034,8 @@ def my_task(self): ) ) worker.spawning_greenlet.join() - self.assertDictEqual(worker.user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) - self.assertDictEqual(worker.target_user_class_occurrences, {"MyUser1": 1, "MyUser2": 2}) + self.assertDictEqual(worker.user_classes_count, {"MyUser1": 1, "MyUser2": 2}) + self.assertDictEqual(worker.target_user_classes_count, {"MyUser1": 1, "MyUser2": 2}) self.assertEqual(worker.target_user_count, 3) worker.quit() From 9a82cf259ac91bb210dbed6271ecfc89313a6115 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 09:17:23 -0400 Subject: [PATCH 081/139] Rename "dummy worker node" to "local worker node" --- locust/runners.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index fe99661ec0..c38fb519bd 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -295,12 +295,11 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): self.target_user_classes_count = weight_users(self.user_classes, user_count) - # Dummy worker node since dispatch_users needs it - dummy_worker_node = WorkerNode(id="dummy") - dummy_worker_node.user_classes_count = self.user_classes_count + local_worker_node = WorkerNode(id="local") + local_worker_node.user_classes_count = self.user_classes_count users_dispatcher = dispatch_users( - worker_nodes=[dummy_worker_node], + worker_nodes=[local_worker_node], user_classes_count=self.target_user_classes_count, spawn_rate=spawn_rate, ) @@ -315,7 +314,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): for dispatched_users in users_dispatcher: user_classes_spawn_count = {} user_classes_stop_count = {} - user_classes_count = dispatched_users[dummy_worker_node.id] + user_classes_count = dispatched_users[local_worker_node.id] logger.info("Updating running test with %d users" % (sum(user_classes_count.values()),)) for user_class, user_class_count in user_classes_count.items(): logger.debug( From 73bc6c1acae5076a3b8547d740a2072b938cf5e8 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 09:36:58 -0400 Subject: [PATCH 082/139] Rename `go_to_next_user_class` to be more meaningful New name is `try_next_user_class_to_stay_balanced_during_ramp_up` which indicates what this function accomplishes. --- locust/dispatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index c982095ff3..16c3098c49 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -201,7 +201,7 @@ def users_to_dispatch_for_current_iteration( ): continue - if go_to_next_user_class( + if try_next_user_class_to_stay_balanced_during_ramp_up( current_user_class, user_classes_count, dispatched_users, effective_balanced_users ): continue @@ -241,7 +241,7 @@ def number_of_dispatched_users_for_user_class(dispatched_users: Dict[str, Dict[s return sum(map(itemgetter(user_class), dispatched_users.values())) -def go_to_next_user_class( +def try_next_user_class_to_stay_balanced_during_ramp_up( current_user_class: str, user_classes_count: Dict[str, int], dispatched_users: Dict[str, Dict[str, int]], From 3a34fe6c61a0d96e899065311bcad356ec86fe46 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 16:27:23 -0400 Subject: [PATCH 083/139] Use `user_count` instead of `number_of_users` for consistency The rest of the codebase already uses `user_count`. --- locust/distribution.py | 34 +++++++++--------- locust/test/test_distribution.py | 60 ++++++++++++++++---------------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 6e1bb560c7..5f5395bafe 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -12,7 +12,7 @@ def weight_users( user_classes: List[Type[User]], - number_of_users: int, + user_count: int, ) -> Dict[str, int]: """ Compute the desired state of users using the weight of each user class. @@ -25,10 +25,10 @@ def weight_users( of users will match the desired one (as dictated by the weight attributes). :param user_classes: the list of user class - :param number_of_users: total number of users + :param user_count: total number of users :return: the set of users to run """ - assert number_of_users >= 0 + assert user_count >= 0 if len(user_classes) == 0: return {} @@ -37,7 +37,7 @@ def weight_users( user_classes_count = {user_class.__name__: 0 for user_class in user_classes} - if number_of_users <= len(user_classes): + if user_count <= len(user_classes): user_classes_count.update( { user_class.__name__: 1 @@ -45,45 +45,45 @@ def weight_users( user_classes, key=attrgetter("weight"), reverse=True, - )[:number_of_users] + )[:user_count] } ) return user_classes_count weights = list(map(attrgetter("weight"), user_classes)) user_classes_count = { - user_class.__name__: round(relative_weight * number_of_users) or 1 + user_class.__name__: round(relative_weight * user_count) or 1 for user_class, relative_weight in zip(user_classes, (weight / sum(weights) for weight in weights)) } - if sum(user_classes_count.values()) == number_of_users: + if sum(user_classes_count.values()) == user_count: return user_classes_count else: user_classes_count = _find_ideal_users_to_add_or_remove( user_classes, - number_of_users - sum(user_classes_count.values()), + user_count - sum(user_classes_count.values()), user_classes_count, ) - assert sum(user_classes_count.values()) == number_of_users + assert sum(user_classes_count.values()) == user_count return user_classes_count def _find_ideal_users_to_add_or_remove( user_classes: List[Type[User]], - number_of_users_to_add_or_remove: int, + user_count_to_add_or_remove: int, user_classes_count: Dict[str, int], ) -> Dict[str, int]: - sign = -1 if number_of_users_to_add_or_remove < 0 else 1 + sign = -1 if user_count_to_add_or_remove < 0 else 1 - number_of_users_to_add_or_remove = abs(number_of_users_to_add_or_remove) + user_count_to_add_or_remove = abs(user_count_to_add_or_remove) - assert number_of_users_to_add_or_remove <= len(user_classes), number_of_users_to_add_or_remove + assert user_count_to_add_or_remove <= len(user_classes), user_count_to_add_or_remove # Formula for combination with replacement # (https://www.tutorialspoint.com/statistics/combination_with_replacement.htm) - number_of_combinations = math.factorial(len(user_classes) + number_of_users_to_add_or_remove - 1) / ( - math.factorial(number_of_users_to_add_or_remove) * math.factorial(len(user_classes) - 1) + number_of_combinations = math.factorial(len(user_classes) + user_count_to_add_or_remove - 1) / ( + math.factorial(user_count_to_add_or_remove) * math.factorial(len(user_classes) - 1) ) # If the number of combinations with replacement is above this threshold, we simply add/remove @@ -93,7 +93,7 @@ def _find_ideal_users_to_add_or_remove( if number_of_combinations <= max_number_of_combinations_threshold: user_classes_count_candidates: Dict[float, Dict[str, int]] = {} - for user_classes_combination in combinations_with_replacement(user_classes, number_of_users_to_add_or_remove): + for user_classes_combination in combinations_with_replacement(user_classes, user_count_to_add_or_remove): user_classes_count_candidate = user_classes_count.copy() for user_class in user_classes_combination: user_classes_count_candidate[user_class.__name__] += sign @@ -108,7 +108,7 @@ def _find_ideal_users_to_add_or_remove( else: user_classes_count_candidate = user_classes_count.copy() - for user_class in user_classes[:number_of_users_to_add_or_remove]: + for user_class in user_classes[:user_count_to_add_or_remove]: user_classes_count_candidate[user_class.__name__] += sign return user_classes_count_candidate diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index c73fa10219..15d47030c6 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -9,13 +9,13 @@ class TestDistribution(unittest.TestCase): def test_distribution_no_user_classes(self): user_classes_count = weight_users( user_classes=[], - number_of_users=0, + user_count=0, ) self.assertDictEqual(user_classes_count, {}) user_classes_count = weight_users( user_classes=[], - number_of_users=1, + user_count=1, ) self.assertDictEqual(user_classes_count, {}) @@ -31,19 +31,19 @@ class User3(User): user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=0, + user_count=0, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=1, + user_count=1, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 0, "User3": 0}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=2, + user_count=2, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 0}) @@ -59,25 +59,25 @@ class User3(User): user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=3, + user_count=3, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=4, + user_count=4, ) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 1, "User3": 1}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=5, + user_count=5, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=6, + user_count=6, ) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 2, "User3": 2}) @@ -93,19 +93,19 @@ class User3(User): user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=0, + user_count=0, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=1, + user_count=1, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 1}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=2, + user_count=2, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 1}) @@ -121,37 +121,37 @@ class User3(User): user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=3, + user_count=3, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=4, + user_count=4, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 2}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=5, + user_count=5, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=6, + user_count=6, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 3}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=10, + user_count=10, ) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 3, "User3": 5}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=11, + user_count=11, ) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 4, "User3": 5}) @@ -167,19 +167,19 @@ class User3(User): user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=0, + user_count=0, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=1, + user_count=1, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 0}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=2, + user_count=2, ) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 1}) @@ -195,37 +195,37 @@ class User3(User): user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=3, + user_count=3, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=4, + user_count=4, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 2}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=5, + user_count=5, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=6, + user_count=6, ) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 3, "User3": 2}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=10, + user_count=10, ) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 4, "User3": 4}) user_classes_count = weight_users( user_classes=[User1, User2, User3], - number_of_users=11, + user_count=11, ) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 5, "User3": 4}) @@ -275,7 +275,7 @@ class User14(User): class User15(User): weight = 69 - for number_of_users in range(1044523783783, 1044523783783 + 1000): + for user_count in range(1044523783783, 1044523783783 + 1000): ts = time.perf_counter() user_classes_count = weight_users( user_classes=[ @@ -295,8 +295,8 @@ class User15(User): User14, User15, ], - number_of_users=number_of_users, + user_count=user_count, ) delta_ms = 1e3 * (time.perf_counter() - ts) - self.assertEqual(sum(user_classes_count.values()), number_of_users) + self.assertEqual(sum(user_classes_count.values()), user_count) self.assertLessEqual(delta_ms, 100) From 3786edd8149298cd3cef4c210574a421cd60b26c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 16:28:57 -0400 Subject: [PATCH 084/139] Relocate comments on implementation details close to code They were in the docstring, now they're close to the relevant code. --- locust/distribution.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 5f5395bafe..b92d5b8343 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -17,13 +17,6 @@ def weight_users( """ Compute the desired state of users using the weight of each user class. - If `number_of_users` is less than `len(user_classes)`, at most one user of each user class - is chosen. User classes with higher weight are chosen first. - - If `number_of_users` is greater than or equal to `len(user_classes)`, at least one user of each - user class will be chosen. The greater `number_of_users` is, the better the actual distribution - of users will match the desired one (as dictated by the weight attributes). - :param user_classes: the list of user class :param user_count: total number of users :return: the set of users to run @@ -37,6 +30,8 @@ def weight_users( user_classes_count = {user_class.__name__: 0 for user_class in user_classes} + # If the number of users is less than the number of user classes, at most one user of each user class + # is chosen. User classes with higher weight are chosen first. if user_count <= len(user_classes): user_classes_count.update( { @@ -50,6 +45,9 @@ def weight_users( ) return user_classes_count + # If the number of users is greater than or equal to the number of user classes, at least one user of each + # user class will be chosen. The greater number of users is, the better the actual distribution + # of users will match the desired one (as dictated by the weight attributes). weights = list(map(attrgetter("weight"), user_classes)) user_classes_count = { user_class.__name__: round(relative_weight * user_count) or 1 From da0623e2432071025d26a0dcd073e621773e2611 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 16:43:54 -0400 Subject: [PATCH 085/139] Add a comment justifying the use of `.copy()` --- locust/distribution.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/locust/distribution.py b/locust/distribution.py index b92d5b8343..78c059a8a3 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -92,6 +92,7 @@ def _find_ideal_users_to_add_or_remove( if number_of_combinations <= max_number_of_combinations_threshold: user_classes_count_candidates: Dict[float, Dict[str, int]] = {} for user_classes_combination in combinations_with_replacement(user_classes, user_count_to_add_or_remove): + # Copy in order to not mutate `user_classes_count` for the parent scope user_classes_count_candidate = user_classes_count.copy() for user_class in user_classes_combination: user_classes_count_candidate[user_class.__name__] += sign @@ -105,6 +106,7 @@ def _find_ideal_users_to_add_or_remove( return user_classes_count_candidates[min(user_classes_count_candidates.keys())] else: + # Copy in order to not mutate `user_classes_count` for the parent scope user_classes_count_candidate = user_classes_count.copy() for user_class in user_classes[:user_count_to_add_or_remove]: user_classes_count_candidate[user_class.__name__] += sign From 314e2dbf012e6d2ae931c4974930dd3cbb33429a Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 16:47:00 -0400 Subject: [PATCH 086/139] Rename `x_2_y` variables to something more natural --- locust/distribution.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 78c059a8a3..e7c644cd4d 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -117,19 +117,19 @@ def distance_from_desired_distribution( user_classes: List[Type[User]], user_classes_count: Dict[str, int], ) -> float: - user_class_2_actual_ratio = { + actual_ratio_of_user_class = { user_class: user_class_count / sum(user_classes_count.values()) for user_class, user_class_count in user_classes_count.items() } - user_class_2_expected_ratio = { + expected_ratio_of_user_class = { user_class.__name__: user_class.weight / sum(map(attrgetter("weight"), user_classes)) for user_class in user_classes } differences = [ - user_class_2_actual_ratio[user_class] - expected_ratio - for user_class, expected_ratio in user_class_2_expected_ratio.items() + actual_ratio_of_user_class[user_class] - expected_ratio + for user_class, expected_ratio in expected_ratio_of_user_class.items() ] return math.sqrt(math.fsum(map(lambda x: x ** 2, differences))) From b5ab2739c42e0c829db38a9378c2ed98363ed6cc Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 16:52:27 -0400 Subject: [PATCH 087/139] Revert improvements for default argument value being mutable It would be best to address this in a separate PR. --- locust/env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/env.py b/locust/env.py index 03da7a855e..0fcd00d079 100644 --- a/locust/env.py +++ b/locust/env.py @@ -75,7 +75,7 @@ class Environment: def __init__( self, *, - user_classes=None, + user_classes=[], shape_class=None, tags=None, exclude_tags=None, @@ -91,7 +91,7 @@ def __init__( else: self.events = Events() - self.user_classes = user_classes or [] + self.user_classes = user_classes self.shape_class = shape_class self.tags = tags self.exclude_tags = exclude_tags From 3239f213ddc67226ef0765e882a98fd0b3ae6dc6 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 17:01:42 -0400 Subject: [PATCH 088/139] Do not use intermediate and useless `users_dispatcher` variable --- locust/runners.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index c38fb519bd..830bc6041a 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -298,12 +298,6 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): local_worker_node = WorkerNode(id="local") local_worker_node.user_classes_count = self.user_classes_count - users_dispatcher = dispatch_users( - worker_nodes=[local_worker_node], - user_classes_count=self.target_user_classes_count, - spawn_rate=spawn_rate, - ) - if self.state != STATE_INIT and self.state != STATE_STOPPED: logger.debug( "Updating running test with %d users, %.2f spawn rate and wait=%r" % (user_count, spawn_rate, wait) @@ -311,7 +305,11 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): self.update_state(STATE_SPAWNING) try: - for dispatched_users in users_dispatcher: + for dispatched_users in dispatch_users( + worker_nodes=[local_worker_node], + user_classes_count=self.target_user_classes_count, + spawn_rate=spawn_rate, + ): user_classes_spawn_count = {} user_classes_stop_count = {} user_classes_count = dispatched_users[local_worker_node.id] From 6ac0fa8fc5215d82988071e6031e880e19d6bdf0 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 17:03:47 -0400 Subject: [PATCH 089/139] Fix wording in comment There was an extra and unnecessary "that" which made the sentence weird. --- locust/runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/runners.py b/locust/runners.py index 830bc6041a..8099df7be9 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -393,7 +393,7 @@ def stop(self): self.spawning_greenlet.kill(block=True) if self.environment.shape_class is not None and self.shape_greenlet is not greenlet.getcurrent(): - # If the test was not started yet and that locust is + # If the test was not started yet and locust is # stopped/quit, shape_greenlet will be None. if self.shape_greenlet is not None: self.shape_greenlet.kill(block=True) From f3db6e1ac45288a6fc629ee75c66d7331555c05a Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 17:05:56 -0400 Subject: [PATCH 090/139] bis: Do not use intermediate and useless `users_dispatcher` variable --- locust/runners.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 8099df7be9..63513d7855 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -634,14 +634,12 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: self.update_state(STATE_SPAWNING) - users_dispatcher = dispatch_users( - worker_nodes=self.clients.ready + self.clients.running + self.clients.spawning, - user_classes_count=self.target_user_classes_count, - spawn_rate=spawn_rate, - ) - try: - for dispatched_users in users_dispatcher: + for dispatched_users in dispatch_users( + worker_nodes=self.clients.ready + self.clients.running + self.clients.spawning, + user_classes_count=self.target_user_classes_count, + spawn_rate=spawn_rate, + ): dispatch_greenlets = Group() for worker_node_id, worker_user_classes_count in dispatched_users.items(): data = { From 121407f5fc3f57e1e97743b2cfeaf1d88dba755a Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 17:21:28 -0400 Subject: [PATCH 091/139] Remove slow and redundant tests in `test_dispatch.py` --- locust/test/test_dispatch.py | 509 ++++++----------------------------- 1 file changed, 81 insertions(+), 428 deletions(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index bb43034b20..22c1c362d1 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -254,7 +254,7 @@ def test_balance_5_users_among_10_workers(self): class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") @@ -262,10 +262,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.15, + spawn_rate=0.5, ) - sleep_time = 1 / 0.15 + sleep_time = 1 / 0.5 ts = time.time() self.assertDictEqual( @@ -380,130 +380,6 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): - worker_node1 = WorkerNode("1") - worker_node2 = WorkerNode("2") - worker_node3 = WorkerNode("3") - - users_dispatcher = dispatch_users( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.5, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 0, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 0, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 0, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") worker_node2 = WorkerNode("2") @@ -515,6 +391,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): spawn_rate=1, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -537,7 +415,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -549,7 +427,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -561,7 +439,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -573,7 +451,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -585,7 +463,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -597,7 +475,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -609,7 +487,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -621,7 +499,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -639,6 +517,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): spawn_rate=2, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -661,7 +541,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -673,7 +553,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -685,7 +565,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -697,7 +577,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -793,6 +673,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): spawn_rate=3, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -815,7 +697,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -827,7 +709,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -845,6 +727,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): spawn_rate=4, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -867,7 +751,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -879,7 +763,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -916,7 +800,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") @@ -927,10 +811,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.15, + spawn_rate=0.5, ) - sleep_time = 1 / 0.15 + sleep_time = 1 / 0.5 ts = time.time() self.assertDictEqual( @@ -1021,109 +905,6 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 1} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 1} - - users_dispatcher = dispatch_users( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.5, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 0, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} @@ -1138,6 +919,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): spawn_rate=1, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1160,7 +943,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1172,7 +955,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1184,7 +967,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1196,7 +979,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1208,7 +991,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1220,7 +1003,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1241,6 +1024,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): spawn_rate=2, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1263,7 +1048,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1275,7 +1060,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1287,7 +1072,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1377,6 +1162,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): spawn_rate=3, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1399,7 +1186,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1411,7 +1198,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1432,6 +1219,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): spawn_rate=4, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1454,7 +1243,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1494,7 +1283,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") @@ -1505,10 +1294,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): users_dispatcher = dispatch_users( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.15, + spawn_rate=0.5, ) - sleep_time = 1 / 0.15 + sleep_time = 1 / 0.5 ts = time.time() self.assertDictEqual( @@ -1563,73 +1352,6 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_15(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = dispatch_users( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.5, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(1.98 <= delta <= 2.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} @@ -1644,6 +1366,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): spawn_rate=1, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1666,7 +1390,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1678,7 +1402,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1690,7 +1414,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1711,6 +1435,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): spawn_rate=2, ) + sleep_time = 1 + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1733,7 +1459,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1745,7 +1471,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1990,6 +1716,8 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): spawn_rate=5, ) + sleep_time = 1 + # total user count = 5 ts = time.time() dispatched_users = next(users_dispatcher) @@ -2026,7 +1754,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 15 ts = time.time() @@ -2045,7 +1773,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 20 ts = time.time() @@ -2064,7 +1792,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 25 ts = time.time() @@ -2083,7 +1811,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 30 ts = time.time() @@ -2102,7 +1830,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 35 ts = time.time() @@ -2121,7 +1849,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 40 ts = time.time() @@ -2140,7 +1868,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 45 ts = time.time() @@ -2159,7 +1887,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 50 ts = time.time() @@ -2178,7 +1906,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 55 ts = time.time() @@ -2197,7 +1925,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 60 ts = time.time() @@ -2216,7 +1944,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 65 ts = time.time() @@ -2235,7 +1963,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 70 ts = time.time() @@ -2254,7 +1982,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) # total user count = 75, User1 = 25, User2 = 50 ts = time.time() @@ -2273,7 +2001,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -2309,6 +2037,8 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw spawn_rate=1, ) + sleep_time = 1 + for dispatch_iteration in range(25): ts = time.time() dispatched_users = next(users_dispatcher) @@ -2320,7 +2050,7 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw if dispatch_iteration == 0: self.assertTrue(0 <= delta <= 0.02, delta) else: - self.assertTrue(0.98 <= delta <= 1.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -2328,83 +2058,6 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw self.assertTrue(0 <= delta <= 0.02, delta) -class TestDispatchUsersToWorkersFuzzy(unittest.TestCase): - def test_dispatch_users_to_workers(self): - """ - This "fuzzy" test uses the "dispatch_users" with various - input parameters to validate that the dispatch logic does not get stuck - in some infinite loop or other unforeseen situations. - """ - weights_iterator = itertools.cycle([5, 4, 9, 4, 9, 8, 4, 10, 1, 5]) - number_of_prior_users_iterator = itertools.cycle([0, 1, 3, 10, 20]) - spawn_rate_multipliers_iterator = itertools.cycle( - [ - 0.770798258958361, - 0.6310883490428525, - 0.08332730831289559, - 0.5498638520309477, - 0.33919312148903324, - 0.275113942104787, - 0.4120114294121081, - 0.9757043117340924, - 0.5950736075658479, - 0.42576147123568686, - 0.001, - ] - ) - - # We repeat the test cases thrice so that we cover enough of the - # values in the above iterators. - for _ in range(5): - for number_of_user_classes in [1, 5, 10, 20, 30, 40, 50, 100]: - for number_of_workers in [1, 2, 5, 10, 30, 80, 100, 500]: - worker_nodes = [] - for i in range(1, number_of_workers + 1): - worker_node = WorkerNode(str(i)) - worker_node.user_classes_count = { - f"User{i}": next(number_of_prior_users_iterator) - for i in range(1, number_of_user_classes + 1) - } - worker_nodes.append(worker_node) - - user_classes_count = { - f"User{i}": next(weights_iterator) for i in range(1, number_of_user_classes + 1) - } - - # We limit the maximum total dispatch to around 10s (i.e. total_number_of_users / 10) - # so that the test does take too much time. - total_number_of_users = sum(user_classes_count.values()) - spawn_rate = max(total_number_of_users / 10, 100 * next(spawn_rate_multipliers_iterator)) - - users_dispatcher = dispatch_users( - worker_nodes=worker_nodes, - user_classes_count=user_classes_count, - spawn_rate=spawn_rate, - ) - - ts = time.perf_counter() - list(users_dispatcher) - expected_spawn_duration = ( - 10 - if number_of_workers < 100 - else 15 - if number_of_workers < 250 - else 20 - if number_of_workers < 400 - else 35 - ) - self.assertLessEqual( - time.perf_counter() - ts, - expected_spawn_duration, - "number_of_user_classes: {} - number_of_workers: {} - total_number_of_users: {} - spawn_rate: {}".format( - number_of_user_classes, - number_of_workers, - total_number_of_users, - spawn_rate, - ), - ) - - class TestNumberOfUsersLeftToDispatch(unittest.TestCase): def test_number_of_users_left_to_dispatch(self): user_classes_count = {"User1": 6, "User2": 2, "User3": 8} From c168529f52927f13f3ff0b3474b0d0c4e5a71ae4 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 21 Jun 2021 17:37:50 -0400 Subject: [PATCH 092/139] Make dict comprehension more readable --- locust/distribution.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locust/distribution.py b/locust/distribution.py index e7c644cd4d..5ce9a033cc 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -49,9 +49,10 @@ def weight_users( # user class will be chosen. The greater number of users is, the better the actual distribution # of users will match the desired one (as dictated by the weight attributes). weights = list(map(attrgetter("weight"), user_classes)) + relative_weights = [weight / sum(weights) for weight in weights] user_classes_count = { user_class.__name__: round(relative_weight * user_count) or 1 - for user_class, relative_weight in zip(user_classes, (weight / sum(weights) for weight in weights)) + for user_class, relative_weight in zip(user_classes, relative_weights) } if sum(user_classes_count.values()) == user_count: From 0b6c7e00e3b98b27687a00899087661198a48e65 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 10:46:50 -0400 Subject: [PATCH 093/139] Raise exception if multiple user classes have the same name --- locust/env.py | 9 ++++++++ locust/test/fake_module1_for_env_test.py | 7 ++++++ locust/test/fake_module2_for_env_test.py | 7 ++++++ locust/test/test_env.py | 11 ++++++++++ locust/test/test_users.py | 27 ++++++++++++++++++++++++ locust/user/users.py | 5 +++++ 6 files changed, 66 insertions(+) create mode 100644 locust/test/fake_module1_for_env_test.py create mode 100644 locust/test/fake_module2_for_env_test.py create mode 100644 locust/test/test_users.py diff --git a/locust/env.py b/locust/env.py index 0fcd00d079..0354574f9f 100644 --- a/locust/env.py +++ b/locust/env.py @@ -1,3 +1,4 @@ +from operator import methodcaller from typing import ( Dict, List, @@ -104,6 +105,14 @@ def __init__( self._filter_tasks_by_tags() + # Validate there's no class with the same name but in different modules + if len(set(user_class.__name__ for user_class in self.user_classes)) != len(self.user_classes): + raise ValueError( + "The following user classes have the same class name: {}".format( + ", ".join(map(methodcaller("fullname"), self.user_classes)) + ) + ) + def _create_runner( self, runner_class: Type[RunnerType], diff --git a/locust/test/fake_module1_for_env_test.py b/locust/test/fake_module1_for_env_test.py new file mode 100644 index 0000000000..68f7dcef75 --- /dev/null +++ b/locust/test/fake_module1_for_env_test.py @@ -0,0 +1,7 @@ +"""Module for locust.test.test_env.TestEnvironment.test_user_classes_with_same_name_is_error""" + +from locust import User + + +class MyUserWithSameName(User): + pass diff --git a/locust/test/fake_module2_for_env_test.py b/locust/test/fake_module2_for_env_test.py new file mode 100644 index 0000000000..68f7dcef75 --- /dev/null +++ b/locust/test/fake_module2_for_env_test.py @@ -0,0 +1,7 @@ +"""Module for locust.test.test_env.TestEnvironment.test_user_classes_with_same_name_is_error""" + +from locust import User + + +class MyUserWithSameName(User): + pass diff --git a/locust/test/test_env.py b/locust/test/test_env.py index b6a4afb61d..099c7ee6b1 100644 --- a/locust/test/test_env.py +++ b/locust/test/test_env.py @@ -7,6 +7,8 @@ task, ) from .testcases import LocustTestCase +from .fake_module1_for_env_test import MyUserWithSameName as MyUserWithSameName1 +from .fake_module2_for_env_test import MyUserWithSameName as MyUserWithSameName2 class TestEnvironment(LocustTestCase): @@ -28,3 +30,12 @@ def my_task(self): environment = Environment(user_classes=[MyUser1, MyUser2]) self.assertDictEqual({"MyUser1": MyUser1, "MyUser2": MyUser2}, environment.user_classes_by_name) + + def test_user_classes_with_same_name_is_error(self): + with self.assertRaises(ValueError) as e: + environment = Environment(user_classes=[MyUserWithSameName1, MyUserWithSameName2]) + + self.assertEqual( + e.exception.args[0], + "The following user classes have the same class name: locust.test.fake_module1_for_env_test.MyUserWithSameName, locust.test.fake_module2_for_env_test.MyUserWithSameName", + ) diff --git a/locust/test/test_users.py b/locust/test/test_users.py new file mode 100644 index 0000000000..943b9cf151 --- /dev/null +++ b/locust/test/test_users.py @@ -0,0 +1,27 @@ +import unittest + +from locust import User + + +class TestUserClass(unittest.TestCase): + class MyClassScopedUser(User): + pass + + def test_fullname_module_scoped(self): + self.assertEqual(MyModuleScopedUser.fullname(), "locust.test.test_users.MyModuleScopedUser") + + def test_fullname_class_scoped(self): + self.assertEqual(self.MyClassScopedUser.fullname(), "locust.test.test_users.TestUserClass.MyClassScopedUser") + + def test_fullname_function_scoped(self): + class MyFunctionScopedUser(User): + pass + + self.assertEqual( + MyFunctionScopedUser.fullname(), + "locust.test.test_users.TestUserClass.test_fullname_function_scoped.MyFunctionScopedUser", + ) + + +class MyModuleScopedUser(User): + pass diff --git a/locust/user/users.py b/locust/user/users.py index a501ca8dd3..6f5221473a 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -199,6 +199,11 @@ def context(self) -> Dict: """ return {} + @classmethod + def fullname(cls) -> str: + """Fully qualified name of the user class, e.g. my_package.my_module.MyUserClass""" + return ".".join(filter(lambda x: x != "", (cls.__module__ + "." + cls.__qualname__).split("."))) + class HttpUser(User): """ From f91539da2528eb7789ce9fc4a512152e70c66c10 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 15:54:03 -0400 Subject: [PATCH 094/139] Minor fix in test: `WorkerNode` takes a string as the name, not an int --- 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 507307ac61..6bb9f863af 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1296,9 +1296,9 @@ def my_task(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner(user_classes=[TestUser]) - master.clients[1] = WorkerNode(1) - master.clients[2] = WorkerNode(2) - master.clients[3] = WorkerNode(3) + master.clients[1] = WorkerNode("1") + master.clients[2] = WorkerNode("2") + master.clients[3] = WorkerNode("3") master.clients[1].state = STATE_INIT master.clients[2].state = STATE_SPAWNING master.clients[3].state = STATE_RUNNING From 496df0a3418f5d29db303a76a762ffdd3e246aee Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 15:59:07 -0400 Subject: [PATCH 095/139] Refactor dispatch to use a single `UsersDispatcher` class Also included: - Apply black with `--skip-magic-trailing-comma` on `dispatch.py` and `test_dispatch.py` - Remove unnecessary test `TestNumberOfUsersLeftToDispatch` - Use `user_count` instead `number_of_users` for consistency with the rest of the codebase - Review wording in code comments in dispatch module - Simplify dispatch by consolidating some logic that was duplicated --- locust/dispatch.py | 699 +++++++++++++++++------------------ locust/runners.py | 6 +- locust/test/test_dispatch.py | 444 ++++++++++------------ 3 files changed, 524 insertions(+), 625 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 16c3098c49..b78ed62224 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -4,32 +4,24 @@ import time from collections import namedtuple from copy import deepcopy -from operator import ( - itemgetter, - methodcaller, - ne, -) -from typing import ( - Dict, - Generator, - List, - TYPE_CHECKING, -) +from operator import itemgetter, methodcaller, ne +from typing import Dict, Generator, List, TYPE_CHECKING import gevent +from collections.abc import Iterator if TYPE_CHECKING: from locust.runners import WorkerNode -def dispatch_users( - worker_nodes, # type: List[WorkerNode] - user_classes_count: Dict[str, int], - spawn_rate: float, -) -> Generator[Dict[str, Dict[str, int]], None, None]: +_DistancesFromIdealDistribution = namedtuple( + "DistancesFromIdealDistribution", "actual_distance actual_distance_with_current_user_class" +) + + +class UsersDispatcher(Iterator): """ - Generator function that dispatches the users - contained `user_classes_count` to the workers. + Iterator that dispatches the users to the workers. The users already running on the workers are also taken into account. @@ -38,396 +30,375 @@ def dispatch_users( local or distributed mode. The spawn rate is only applied when additional users are needed. - Hence, if `user_classes_count` contains less users than the ones running right now, + Hence, if the desired user count contains less users than what is currently running, the dispatcher won't wait and will only run for - one iteration. The rationale for not stopping users at a rate of `spawn_rate` + one iteration. The rationale for not stopping users at the spawn rate is that stopping them is a blocking operation, especially when - a `stop_timeout` is specified. When a stop timeout is specified combined with users having long-running tasks, - attempting to to stop the users at `spawn_rate` will lead to weird behaviours (users being killed even though the - stop timeout is not reached yey). - - :param worker_nodes: List of worker nodes - :param user_classes_count: Desired number of users for each class - :param spawn_rate: The spawn rate + a stop timeout is specified. When a stop timeout is specified combined with users having long-running tasks, + attempting to stop the users at a spawn rate will lead to weird behaviours (users being killed even though the + stop timeout is not reached yet). """ - # NOTE: We use "sorted" in some places in this module. It is done to ensure repeatable behaviour. - # This is especially important when iterating over a dictionary which, prior to py3.7, was - # completely unordered. For >=Py3.7, a dictionary keeps the insertion order. Even then, - # it is safer to sort the keys when repeatable behaviour is required. - - worker_nodes = sorted(worker_nodes, key=lambda w: w.id) - # This represents the already running users among the workers - initial_dispatched_users = { - worker_node.id: { - user_class: worker_node.user_classes_count.get(user_class, 0) for user_class in user_classes_count.keys() - } - for worker_node in worker_nodes - } - - # This represents the desired users distribution among the workers - balanced_users = balance_users_among_workers( - worker_nodes, - user_classes_count, - ) - - # This represents the desired users distribution minus the already running users among the workers. - # The values inside this dictionary are updated during the current dispatch cycle. For example, - # if we dispatch 1 user of UserClass1 to worker 1, then we will decrement by 1 the user count - # for UserClass1 of worker 1. Naturally, the current dispatch cycle is done once all the values - # reach zero. - effective_balanced_users = { - worker_node.id: { - user_class: max( - 0, - balanced_users[worker_node.id][user_class] - initial_dispatched_users[worker_node.id][user_class], - ) - for user_class in user_classes_count.keys() + def __init__( + self, + worker_nodes, # type: List[WorkerNode] + user_classes_count: Dict[str, int], + spawn_rate: float, + ): + """ + :param worker_nodes: List of worker nodes + :param user_classes_count: Desired number of users for each class + :param spawn_rate: The spawn rate + """ + # NOTE: We use "sorted" in some places in this module. It is done to ensure repeatable behaviour. + # This is especially important when iterating over a dictionary which, prior to py3.7, was + # completely unordered. For >=Py3.7, a dictionary keeps the insertion order. Even then, + # it is safer to sort the keys when repeatable behaviour is required. + self._worker_nodes = sorted(worker_nodes, key=lambda w: w.id) + + self._user_classes_count = user_classes_count + + self._spawn_rate = spawn_rate + + # This represents the desired users distribution minus the already running users among the workers. + # The values inside this dictionary are updated during the current dispatch cycle. For example, + # if we dispatch 1 user of UserClass1 to worker 1, then we will decrement by 1 the user count + # for UserClass1 of worker 1. Naturally, the current dispatch cycle is done once all the values + # reach zero. + self._effective_assigned_users = { + worker_node.id: { + user_class: max( + 0, + self._desired_users_assigned_to_workers()[worker_node.id][user_class] + - self._initial_dispatched_users()[worker_node.id][user_class], + ) + for user_class in self._user_classes_count.keys() + } + for worker_node in self._worker_nodes } - for worker_node in worker_nodes - } - - number_of_users_per_dispatch = max(1, math.floor(spawn_rate)) - - wait_between_dispatch = number_of_users_per_dispatch / spawn_rate - - # We use deepcopy because we will update the values inside `dispatched_users` - # to keep track of the number of dispatched users for the current dispatch cycle. - # It is essentially the same thing as for the `effective_balanced_users` dictionary, - # but in reverse. - dispatched_users = deepcopy(initial_dispatched_users) - - # The amount of users in each user class - # is less than the desired amount - less_users_than_desired = all( - number_of_dispatched_users_for_user_class(dispatched_users, user_class) - < sum(map(itemgetter(user_class), effective_balanced_users.values())) - for user_class in user_classes_count.keys() - ) - - if less_users_than_desired: - while sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - users_to_dispatch = users_to_dispatch_for_current_iteration( - user_classes_count, - dispatched_users, - effective_balanced_users, - balanced_users, - number_of_users_per_dispatch, - ) - ts = time.perf_counter() - yield users_to_dispatch - if sum(sum(x.values()) for x in effective_balanced_users.values()) > 0: - delta = time.perf_counter() - ts - sleep_duration = max(0.0, wait_between_dispatch - delta) - assert sleep_duration <= 10, sleep_duration - gevent.sleep(sleep_duration) - - elif ( - number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) - <= number_of_users_per_dispatch - ): - yield balanced_users - - else: - while not all_users_have_been_dispatched(effective_balanced_users): - users_to_dispatch = users_to_dispatch_for_current_iteration( - user_classes_count, - dispatched_users, - effective_balanced_users, - balanced_users, - number_of_users_per_dispatch, - ) + self._user_count_per_dispatch = max(1, math.floor(self._spawn_rate)) - ts = time.perf_counter() - yield users_to_dispatch - delta = time.perf_counter() - ts - if not all_users_have_been_dispatched(effective_balanced_users): - sleep_duration = max(0.0, wait_between_dispatch - delta) - else: - # We don't need to sleep if there's no more dispatch iteration - sleep_duration = 0 - assert sleep_duration <= 10, sleep_duration - gevent.sleep(sleep_duration) - - -def users_to_dispatch_for_current_iteration( - user_classes_count: Dict[str, int], - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], - balanced_users: Dict[str, Dict[str, int]], - number_of_users_per_dispatch: int, -) -> Dict[str, Dict[str, int]]: - if all( - number_of_dispatched_users_for_user_class(dispatched_users, user_class) >= user_count - for user_class, user_count in user_classes_count.items() - ): - # User count for every user class is greater than or equal to the target user count of each class. - # This means that we're at the last iteration of this dispatch cycle. If some user classes are in - # excess, this last iteration will stop those excess users. - dispatched_users.update(balanced_users) - effective_balanced_users.update( - { - worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} - for worker_node_id, user_classes_count in dispatched_users.items() + self._wait_between_dispatch = self._user_count_per_dispatch / self._spawn_rate + + # We use deepcopy because we will update the values inside `dispatched_users` + # to keep track of the number of dispatched users for the current dispatch cycle. + # It is essentially the same thing as for the `effective_assigned_users` dictionary, + # but in reverse. + self._dispatched_users = deepcopy(self._initial_dispatched_users()) + + self.__dispatch_generator = self._dispatch_generator() + + @functools.lru_cache() + def _initial_dispatched_users(self) -> Dict[str, Dict[str, int]]: + """Represents the already running users among the workers at the start of the dispatch""" + return { + worker_node.id: { + user_class: worker_node.user_classes_count.get(user_class, 0) + for user_class in self._user_classes_count.keys() } + for worker_node in self._worker_nodes + } + + def __next__(self) -> Dict[str, Dict[str, int]]: + return next(self.__dispatch_generator) + + def _dispatch_generator(self) -> Generator[Dict[str, Dict[str, int]], None, None]: + """Main iterator logic for dispatching users during a ramp-up""" + if self._desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration: + yield self._desired_users_assigned_to_workers() + + else: + while not self._all_users_have_been_dispatched: + ts = time.perf_counter() + yield self._users_to_dispatch_for_current_iteration + if not self._all_users_have_been_dispatched: + delta = time.perf_counter() - ts + sleep_duration = max(0.0, self._wait_between_dispatch - delta) + assert sleep_duration <= 10, sleep_duration + gevent.sleep(sleep_duration) + + @property + def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(self) -> bool: + # The following calculates the number of users left to dispatch + # taking into account workers that have an excess of users. + user_count_left_to_dispatch_excluding_excess_users = sum( + max( + 0, + sum(map(itemgetter(user_class), self._desired_users_assigned_to_workers().values())) + - self._dispatched_user_class_count(user_class), + ) + for user_class in self._user_classes_count.keys() ) - else: - ts_dispatch = time.perf_counter() + return self._user_count_per_dispatch >= user_count_left_to_dispatch_excluding_excess_users - number_of_workers = len(effective_balanced_users) + @property + def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: + """ + Compute the users to dispatch for the current dispatch iteration. + """ + if all( + self._dispatched_user_class_count(user_class) >= user_count + for user_class, user_count in self._user_classes_count.items() + ): + # User count for every user class is greater than or equal to the target user count of each class. + # This means that we're at the last iteration of this dispatch cycle. If some user classes are in + # excess, this last iteration will stop those excess users. + self._dispatched_users.update(self._desired_users_assigned_to_workers()) + self._effective_assigned_users.update( + { + worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} + for worker_node_id, user_classes_count in self._dispatched_users.items() + } + ) - number_of_users_in_current_dispatch = 0 + else: + ts_dispatch = time.perf_counter() - for i, current_user_class in enumerate(itertools.cycle(sorted(user_classes_count.keys()))): - # For large number of user classes and large number of workers, this assertion might fail. - # If this happens, you can remove it or increase the threshold. Right now, the assertion - # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). - assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) + user_count_in_current_dispatch = 0 - if all_users_have_been_dispatched(effective_balanced_users): - break + for i, current_user_class in enumerate(itertools.cycle(sorted(self._user_classes_count.keys()))): + # For large number of user classes and large number of workers, this assertion might fail. + # If this happens, you can remove it or increase the threshold. Right now, the assertion + # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). + assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) - if all( - number_of_dispatched_users_for_user_class(dispatched_users, user_class) >= user_count - for user_class, user_count in user_classes_count.items() - ): - break + if self._all_users_have_been_dispatched: + break - if ( - number_of_dispatched_users_for_user_class(dispatched_users, current_user_class) - >= user_classes_count[current_user_class] - ): - continue + if all( + self._dispatched_user_class_count(user_class) >= user_count + for user_class, user_count in self._user_classes_count.items() + ): + break - if try_next_user_class_to_stay_balanced_during_ramp_up( - current_user_class, user_classes_count, dispatched_users, effective_balanced_users - ): - continue + if ( + self._dispatched_user_class_count(current_user_class) + >= self._user_classes_count[current_user_class] + ): + continue - for j, worker_node_id in enumerate(itertools.cycle(sorted(effective_balanced_users.keys()))): - assert j < int( - 2 * number_of_workers - ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if effective_balanced_users[worker_node_id][current_user_class] == 0: + if self._try_next_user_class_to_stay_balanced_during_ramp_up(current_user_class): continue - dispatched_users[worker_node_id][current_user_class] += 1 - effective_balanced_users[worker_node_id][current_user_class] -= 1 - number_of_users_in_current_dispatch += 1 - break - if number_of_users_in_current_dispatch == number_of_users_per_dispatch: - break + for j, worker_node_id in enumerate(itertools.cycle(sorted(self._effective_assigned_users.keys()))): + assert j < int( + 2 * self._number_of_workers + ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + if self._effective_assigned_users[worker_node_id][current_user_class] == 0: + continue + self._dispatched_users[worker_node_id][current_user_class] += 1 + self._effective_assigned_users[worker_node_id][current_user_class] -= 1 + user_count_in_current_dispatch += 1 + break + + if user_count_in_current_dispatch == self._user_count_per_dispatch: + break + + self._assert_computation_duration_of_dispatch_is_reasonable(duration=time.perf_counter() - ts_dispatch) + + return { + worker_node_id: dict(sorted(user_classes_count.items(), key=itemgetter(0))) + for worker_node_id, user_classes_count in sorted(self._dispatched_users.items(), key=itemgetter(0)) + } - # Another assertion to safeguard against unforeseen situations. Ideally, + def _assert_computation_duration_of_dispatch_is_reasonable(self, duration: float) -> None: + # Safeguard against unforeseen performance issues. Ideally, # we want each dispatch loop to be as short as possible to compute, but with # a large amount of workers/user classes, it can take longer to come up with the dispatch solution. # If the assertion is raised, then it could be a sign that the code needs to be optimized for the # situation that caused the assertion to be raised. - assert time.perf_counter() - ts_dispatch < ( - 0.5 if number_of_workers < 100 else 1 if number_of_workers < 250 else 1.5 if number_of_workers < 350 else 3 + assert duration < ( + 0.5 + if self._number_of_workers < 100 + else 1 + if self._number_of_workers < 250 + else 1.5 + if self._number_of_workers < 350 + else 3 ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( - time.perf_counter() - ts_dispatch, number_of_workers, len(user_classes_count) + duration, self._number_of_workers, len(self._user_classes_count) ) - return { - worker_node_id: dict(sorted(user_classes_count.items(), key=itemgetter(0))) - for worker_node_id, user_classes_count in sorted(dispatched_users.items(), key=itemgetter(0)) - } - - -def number_of_dispatched_users_for_user_class(dispatched_users: Dict[str, Dict[str, int]], user_class: str) -> int: - return sum(map(itemgetter(user_class), dispatched_users.values())) - - -def try_next_user_class_to_stay_balanced_during_ramp_up( - current_user_class: str, - user_classes_count: Dict[str, int], - dispatched_users: Dict[str, Dict[str, int]], - effective_balanced_users: Dict[str, Dict[str, int]], -) -> bool: - """ - Whether to skip to next user class or not. This is done so that - the distribution of user class stays approximately balanced during - a ramp up. - """ - dispatched_user_classes_count = { - user_class: sum(map(itemgetter(user_class), dispatched_users.values())) - for user_class in user_classes_count.keys() - } - - if all(user_count > 0 for user_count in dispatched_user_classes_count.values()): - # We're here because each user class have at least one user running. Thus, - # we need to ensure that the distribution of users corresponds to the weights. - if not current_user_class_will_keep_distribution_better_than_all_other_user_classes( - current_user_class, user_classes_count, dispatched_user_classes_count - ): - return True - else: - return False - - else: - # Because each user class doesn't have at least one running user, we use a simpler strategy - # that make sure each user class appears once. - for user_class in filter(functools.partial(ne, current_user_class), sorted(user_classes_count.keys())): - if sum(map(itemgetter(user_class), effective_balanced_users.values())) == 0: - # No more users of class `user_class` to dispatch - continue - if dispatched_user_classes_count[current_user_class] - dispatched_user_classes_count[user_class] >= 1: - # There's already enough users for `current_user_class` in the current dispatch. Hence, we should - # not consider `current_user_class` and go to the next user class instead. - return True - return False - + @property + def _number_of_workers(self) -> int: + return len(self._effective_assigned_users) + + @functools.lru_cache() + def _desired_users_assigned_to_workers(self) -> Dict[str, Dict[str, int]]: + """ + Assign users to workers so that each worker gets around + the same number of users of each user class + """ + assigned_users = { + worker_node.id: {user_class: 0 for user_class in sorted(self._user_classes_count.keys())} + for worker_node in self._worker_nodes + } -def current_user_class_will_keep_distribution_better_than_all_other_user_classes( - current_user_class: str, - user_classes_count: Dict[str, int], - dispatched_user_classes_count: Dict[str, int], -) -> bool: - distances = get_distances_from_ideal_distribution( - current_user_class, user_classes_count, dispatched_user_classes_count - ) - if distances.actual_distance_with_current_user_class > distances.actual_distance and all( - not current_user_class_will_keep_distribution(user_class, user_classes_count, dispatched_user_classes_count) - for user_class in user_classes_count.keys() - if user_class != current_user_class - ): - # If we are here, it means that if one user of `current_user_class` is added - # then the distribution will be the best we can get. In other words, adding - # one user of any other user class won't yield a better distribution. - return True - if distances.actual_distance_with_current_user_class <= distances.actual_distance: - return True - return False - - -def current_user_class_will_keep_distribution( - current_user_class: str, - user_classes_count: Dict[str, int], - dispatched_user_classes_count: Dict[str, int], -) -> bool: - distances = get_distances_from_ideal_distribution( - current_user_class, user_classes_count, dispatched_user_classes_count - ) - if distances.actual_distance_with_current_user_class <= distances.actual_distance: - return True - return False - - -# `actual_distance` corresponds to the distance from the ideal distribution for the current -# dispatched users. `actual_distance_with_current_user_class` represents the distance -# from the ideal distribution if we were to add one user of the given `current_user_class`. -# Thus, we want to find the best user class, in which to add a user, that will give us -# an `actual_distance_with_current_user_class` less than `actual_distance`. -DistancesFromIdealDistribution = namedtuple( - "DistancesFromIdealDistribution", "actual_distance actual_distance_with_current_user_class" -) + # We need to copy to prevent modifying `user_classes_count`. + user_classes_count = self._user_classes_count.copy() + user_count = sum(user_classes_count.values()) -def get_distances_from_ideal_distribution( - current_user_class: str, - user_classes_count: Dict[str, int], - dispatched_user_classes_count: Dict[str, int], -) -> DistancesFromIdealDistribution: - user_classes = list(user_classes_count.keys()) - desired_weights = [user_classes_count[user_class] / sum(user_classes_count.values()) for user_class in user_classes] - actual_weights = [ - dispatched_user_classes_count[user_class] / sum(dispatched_user_classes_count.values()) - for user_class in user_classes - ] - actual_weights_with_current_user_class = [ - ( - dispatched_user_classes_count[user_class] + 1 - if user_class == current_user_class - else dispatched_user_classes_count[user_class] - ) - / (sum(dispatched_user_classes_count.values()) + 1) - for user_class in user_classes - ] - actual_distance = math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, desired_weights)))) - actual_distance_with_current_user_class = math.sqrt( - sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights_with_current_user_class, desired_weights))) - ) - return DistancesFromIdealDistribution(actual_distance, actual_distance_with_current_user_class) - - -def number_of_users_left_to_dispatch( - dispatched_users: Dict[str, Dict[str, int]], - balanced_users: Dict[str, Dict[str, int]], - user_classes_count: Dict[str, int], -) -> int: - return sum( - max( - 0, - sum(map(itemgetter(user_class), balanced_users.values())) - - sum(map(itemgetter(user_class), dispatched_users.values())), - ) - for user_class in user_classes_count.keys() - ) - - -def all_users_have_been_dispatched(effective_balanced_users: Dict[str, Dict[str, int]]) -> bool: - return sum(map(sum, map(dict.values, effective_balanced_users.values()))) == 0 + # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. + users_per_worker, remainder = divmod(user_count, len(self._worker_nodes)) + for user_class in sorted(user_classes_count.keys()): + if sum(user_classes_count.values()) == 0: + # No more users of any user class to assign to workers, so we can exit this loop. + break -def balance_users_among_workers( - worker_nodes, # type: List[WorkerNode] - user_classes_count: Dict[str, int], -) -> Dict[str, Dict[str, int]]: - """ - Balance the users among the workers so that - each worker gets around the same number of users of each user class - """ - balanced_users = { - worker_node.id: {user_class: 0 for user_class in sorted(user_classes_count.keys())} - for worker_node in worker_nodes - } + # Assign users of `user_class` to the workers in a round-robin fashion. + for worker_node in itertools.cycle(self._worker_nodes): + if user_classes_count[user_class] == 0: + break - # We need to copy to prevent modifying `user_classes_count` for the parent scopes. - user_classes_count = user_classes_count.copy() + number_of_users_left_to_assign = user_count - self._number_of_assigned_users_across_workers( + assigned_users + ) - total_users = sum(user_classes_count.values()) + if ( + self._number_of_assigned_users_for_worker(assigned_users, worker_node) == users_per_worker + and number_of_users_left_to_assign > remainder + ): + continue - # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. - users_per_worker, remainder = divmod(total_users, len(worker_nodes)) + elif ( + self._number_of_assigned_users_for_worker(assigned_users, worker_node) == users_per_worker + 1 + and number_of_users_left_to_assign < remainder + ): + continue - for user_class in sorted(user_classes_count.keys()): - if sum(user_classes_count.values()) == 0: - # No more users of any user class to assign to workers, so we can exit this loop. - break + assigned_users[worker_node.id][user_class] += 1 + user_classes_count[user_class] -= 1 + + return assigned_users + + @staticmethod + def _number_of_assigned_users_for_worker( + assigned_users: Dict[str, Dict[str, int]], worker_node # type: WorkerNode + ) -> int: + return sum(assigned_users[worker_node.id].values()) + + @staticmethod + def _number_of_assigned_users_across_workers(assigned_users: Dict[str, Dict[str, int]]) -> int: + return sum(map(sum, map(methodcaller("values"), assigned_users.values()))) + + @property + def _all_users_have_been_dispatched(self) -> bool: + return self._user_count_left_to_dispatch == 0 + + @property + def _user_count_left_to_dispatch(self) -> int: + return sum(map(sum, map(dict.values, self._effective_assigned_users.values()))) + + def _try_next_user_class_to_stay_balanced_during_ramp_up(self, current_user_class: str) -> bool: + """ + Whether to skip to next user class or not. This is done so that + the distribution of user class stays approximately balanced during + a ramp up. + """ + # For performance reasons, we use `functools.lru_cache()` on the `self._dispatched_user_classes_count` + # method because its value does not change within the scope of the current method. However, the next time + # `self._try_next_user_class_to_stay_balanced_during_ramp_up` is invoked, we need + # `self._dispatched_user_classes_count` to be recomputed. + self._dispatched_user_classes_count.cache_clear() + + if all(user_count > 0 for user_count in self._dispatched_user_classes_count().values()): + # We're here because each user class have at least one user running. Thus, + # we need to ensure that the distribution of users corresponds to the weights. + if not self._current_user_class_will_keep_distribution_better_than_all_other_user_classes( + current_user_class + ): + return True + else: + return False - # Assign users of `user_class` to the workers in a round-robin fashion. - for worker_node in itertools.cycle(worker_nodes): - if user_classes_count[user_class] == 0: - break + else: + # Because each user class doesn't have at least one running user, we use a simpler strategy + # that make sure each user class appears once. + for next_user_class in filter( + functools.partial(ne, current_user_class), sorted(self._user_classes_count.keys()) + ): + # TODO: Put in function `user_class_count_left_to_dispatch` + if sum(map(itemgetter(next_user_class), self._effective_assigned_users.values())) == 0: + # No more users of class `next_user_class` to dispatch + continue + if ( + self._dispatched_user_classes_count()[current_user_class] + - self._dispatched_user_classes_count()[next_user_class] + >= 1 + ): + # There's already enough users for `current_user_class` in the current dispatch. Hence, we should + # not consider `current_user_class` and go to the next user class instead. + return True + return False - number_of_users_left_to_assign = total_users - number_of_assigned_users_across_workers(balanced_users) + def _dispatched_user_class_count(self, user_class: str) -> int: + """Number of dispatched users for the given user class""" + return sum(map(itemgetter(user_class), self._dispatched_users.values())) + + def _current_user_class_will_keep_distribution_better_than_all_other_user_classes( + self, current_user_class: str + ) -> bool: + distances = self._distances_from_ideal_distribution(current_user_class) + if distances.actual_distance_with_current_user_class > distances.actual_distance and all( + not self._current_user_class_will_keep_distribution(user_class) + for user_class in self._user_classes_count.keys() + if user_class != current_user_class + ): + # If we are here, it means that if one user of `current_user_class` is added + # then the distribution will be the best we can get. In other words, adding + # one user of any other user class won't yield a better distribution. + return True + if distances.actual_distance_with_current_user_class <= distances.actual_distance: + return True + return False - if ( - number_of_assigned_users_for_worker(balanced_users, worker_node) == users_per_worker - and number_of_users_left_to_assign > remainder - ): - continue + def _current_user_class_will_keep_distribution(self, current_user_class: str) -> bool: + distances = self._distances_from_ideal_distribution(current_user_class) + if distances.actual_distance_with_current_user_class <= distances.actual_distance: + return True + return False - elif ( - number_of_assigned_users_for_worker(balanced_users, worker_node) == users_per_worker + 1 - and number_of_users_left_to_assign < remainder - ): - continue + def _distances_from_ideal_distribution(self, current_user_class) -> _DistancesFromIdealDistribution: + user_classes = list(self._user_classes_count.keys()) - balanced_users[worker_node.id][user_class] += 1 - user_classes_count[user_class] -= 1 + desired_weights = [ + self._user_classes_count[user_class] / sum(self._user_classes_count.values()) for user_class in user_classes + ] - return balanced_users + actual_weights = [ + self._dispatched_user_classes_count()[user_class] / sum(self._dispatched_user_classes_count().values()) + for user_class in user_classes + ] + actual_weights_with_current_user_class = [ + ( + self._dispatched_user_classes_count()[user_class] + 1 + if user_class == current_user_class + else self._dispatched_user_classes_count()[user_class] + ) + / (sum(self._dispatched_user_classes_count().values()) + 1) + for user_class in user_classes + ] -def number_of_assigned_users_for_worker( - balanced_users: Dict[str, Dict[str, int]], worker_node # type: WorkerNode -) -> int: - return sum(balanced_users[worker_node.id].values()) + actual_distance = math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, desired_weights)))) + actual_distance_with_current_user_class = math.sqrt( + sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights_with_current_user_class, desired_weights))) + ) -def number_of_assigned_users_across_workers(balanced_users: Dict[str, Dict[str, int]]) -> int: - return sum(map(sum, map(methodcaller("values"), balanced_users.values()))) + # `actual_distance` corresponds to the distance from the ideal distribution given the + # users dispatched at this time. `actual_distance_with_current_user_class` represents the distance + # from the ideal distribution if we were to add one user of the given `current_user_class`. + # Thus, we want to find the best user class, in which to add a user, that will give us + # an `actual_distance_with_current_user_class` less than `actual_distance`. + return _DistancesFromIdealDistribution(actual_distance, actual_distance_with_current_user_class) + + @functools.lru_cache() + def _dispatched_user_classes_count(self) -> Dict[str, int]: + return { + user_class: self._dispatched_user_class_count(user_class) for user_class in self._user_classes_count.keys() + } diff --git a/locust/runners.py b/locust/runners.py index 63513d7855..19a7f7c3db 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -22,7 +22,7 @@ from gevent.pool import Group from . import User -from .dispatch import dispatch_users +from .dispatch import UsersDispatcher from .distribution import weight_users from .exception import RPCError from .log import greenlet_exception_logger @@ -305,7 +305,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): self.update_state(STATE_SPAWNING) try: - for dispatched_users in dispatch_users( + for dispatched_users in UsersDispatcher( worker_nodes=[local_worker_node], user_classes_count=self.target_user_classes_count, spawn_rate=spawn_rate, @@ -635,7 +635,7 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: self.update_state(STATE_SPAWNING) try: - for dispatched_users in dispatch_users( + for dispatched_users in UsersDispatcher( worker_nodes=self.clients.ready + self.clients.running + self.clients.spawning, user_classes_count=self.target_user_classes_count, spawn_rate=spawn_rate, diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 22c1c362d1..5f872b8f10 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -3,254 +3,271 @@ import unittest from typing import Dict -from locust.dispatch import ( - balance_users_among_workers, - dispatch_users, - number_of_users_left_to_dispatch, -) +from locust.dispatch import UsersDispatcher from locust.runners import WorkerNode -class TestBalanceUsersAmongWorkers(unittest.TestCase): - def test_balance_users_among_1_worker(self): +class TestAssignUsersToWorkers(unittest.TestCase): + def test_assign_users_to_1_worker(self): worker_node1 = WorkerNode("1") - balanced_users = balance_users_among_workers( - worker_nodes=[worker_node1], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1 + ) + self.assertDictEqual( + users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 3, "User2": 3, "User3": 3}} ) - self.assertDictEqual(balanced_users, {"1": {"User1": 3, "User2": 3, "User3": 3}}) - balanced_users = balance_users_among_workers( - worker_nodes=[worker_node1], - user_classes_count={"User1": 5, "User2": 4, "User3": 2}, + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1], user_classes_count={"User1": 5, "User2": 4, "User3": 2}, spawn_rate=1 + ) + self.assertDictEqual( + users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 5, "User2": 4, "User3": 2}} ) - self.assertDictEqual(balanced_users, {"1": {"User1": 5, "User2": 4, "User3": 2}}) - balanced_users = balance_users_among_workers( - worker_nodes=[worker_node1], - user_classes_count={"User1": 1, "User2": 1, "User3": 1}, + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1], user_classes_count={"User1": 1, "User2": 1, "User3": 1}, spawn_rate=1 + ) + self.assertDictEqual( + users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 1, "User2": 1, "User3": 1}} ) - self.assertDictEqual(balanced_users, {"1": {"User1": 1, "User2": 1, "User3": 1}}) - balanced_users = balance_users_among_workers( - worker_nodes=[worker_node1], - user_classes_count={"User1": 1, "User2": 1, "User3": 0}, + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1], user_classes_count={"User1": 1, "User2": 1, "User3": 0}, spawn_rate=1 + ) + self.assertDictEqual( + users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 1, "User2": 1, "User3": 0}} ) - self.assertDictEqual(balanced_users, {"1": {"User1": 1, "User2": 1, "User3": 0}}) - balanced_users = balance_users_among_workers( - worker_nodes=[worker_node1], - user_classes_count={"User1": 0, "User2": 0, "User3": 0}, + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1], user_classes_count={"User1": 0, "User2": 0, "User3": 0}, spawn_rate=1 + ) + self.assertDictEqual( + users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 0, "User2": 0, "User3": 0}} ) - self.assertDictEqual(balanced_users, {"1": {"User1": 0, "User2": 0, "User3": 0}}) - def test_balance_users_among_3_workers(self): + def test_assign_users_to_3_workers(self): worker_node1 = WorkerNode("1") worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=1, ) expected_balanced_users = { "1": {"User1": 1, "User2": 1, "User3": 1}, "2": {"User1": 1, "User2": 1, "User3": 1}, "3": {"User1": 1, "User2": 1, "User3": 1}, } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 5, "User2": 4, "User3": 2}, + spawn_rate=1, ) expected_balanced_users = { "1": {"User1": 2, "User2": 1, "User3": 1}, "2": {"User1": 2, "User2": 1, "User3": 1}, "3": {"User1": 1, "User2": 2, "User3": 0}, } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 1, "User2": 1, "User3": 1}, + spawn_rate=1, ) expected_balanced_users = { "1": {"User1": 1, "User2": 0, "User3": 0}, "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 1}, } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 1, "User2": 1, "User3": 0}, + spawn_rate=1, ) expected_balanced_users = { "1": {"User1": 1, "User2": 0, "User3": 0}, "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 0, "User2": 0, "User3": 0}, + spawn_rate=1, ) expected_balanced_users = { "1": {"User1": 0, "User2": 0, "User3": 0}, "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - def test_balance_5_users_among_10_workers(self): - worker_nodes = [WorkerNode(str(i)) for i in range(1, 11)] + def test_assign_5_users_to_10_workers(self): + # Prepend "0" to worker name under 10, because workers are sorted in alphabetical orders. + # It's simply makes the test easier to read, but in reality, the worker "10" would come + # before worker "1". + worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 11)] - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 10, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "6": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "01": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "06": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "07": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 5}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "01": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "06": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "07": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 5, "User3": 5, "User4": 5, "User5": 6}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "01": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "06": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "07": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 5, "User3": 5, "User4": 6, "User5": 6}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "01": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "06": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "07": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 5, "User3": 6, "User4": 6, "User5": 6}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "6": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "7": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users + "01": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "06": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "07": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 6}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "6": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users - "7": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "01": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "06": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 3 users + "07": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 7}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "6": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users - "7": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "01": {"User1": 2, "User2": 1, "User3": 0, "User4": 0, "User5": 1}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "06": {"User1": 1, "User2": 1, "User3": 1, "User4": 0, "User5": 1}, # 4 users + "07": {"User1": 1, "User2": 0, "User3": 1, "User4": 1, "User5": 0}, # 3 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) - balanced_users = balance_users_among_workers( + users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, user_classes_count={"User1": 11, "User2": 6, "User3": 6, "User4": 6, "User5": 11}, + spawn_rate=1, ) expected_balanced_users = { - "1": {"User1": 2, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 4 users - "2": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users - "3": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users - "4": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users - "5": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users - "6": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users - "7": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 2}, # 4 users - "8": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users - "9": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users + "01": {"User1": 2, "User2": 1, "User3": 1, "User4": 0, "User5": 0}, # 4 users + "02": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "03": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "04": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "05": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "06": {"User1": 1, "User2": 1, "User3": 1, "User4": 1, "User5": 0}, # 4 users + "07": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 2}, # 4 users + "08": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users + "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users } - self.assertDictEqual(balanced_users, expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): @@ -259,7 +276,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.5, @@ -385,7 +402,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1, @@ -511,7 +528,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2, @@ -589,7 +606,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2.4, @@ -667,7 +684,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=3, @@ -721,7 +738,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=4, @@ -775,7 +792,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node2 = WorkerNode("2") worker_node3 = WorkerNode("3") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=9, @@ -808,7 +825,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.5, @@ -913,7 +930,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1, @@ -1018,7 +1035,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2, @@ -1087,7 +1104,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2.4, @@ -1156,7 +1173,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=3, @@ -1213,7 +1230,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=4, @@ -1258,7 +1275,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 1} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=9, @@ -1291,7 +1308,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=0.5, @@ -1360,7 +1377,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1, @@ -1429,7 +1446,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2, @@ -1486,7 +1503,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=2.4, @@ -1543,7 +1560,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=3, @@ -1574,7 +1591,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=4, @@ -1605,7 +1622,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 7} - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=9, @@ -1639,7 +1656,7 @@ def test_dispatch_users_to_3_workers(self): worker_node3.user_classes_count = {"User2": 7} for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=spawn_rate, @@ -1673,7 +1690,7 @@ def test_dispatch_users_to_3_workers(self): worker_node3.user_classes_count = {"User1": 1, "User2": 1, "User3": 1} for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=spawn_rate, @@ -1710,7 +1727,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): worker_node3 = WorkerNode("3") worker_node4 = WorkerNode("4") - users_dispatcher = dispatch_users( + users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3, worker_node4], user_classes_count={"User1": 25, "User2": 50}, spawn_rate=5, @@ -1721,10 +1738,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 5 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 2, "User2": 3}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 2, "User2": 3}) self.assertDictEqual( dispatched_users, { @@ -1740,10 +1754,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 10 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 4, "User2": 6}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 4, "User2": 6}) self.assertDictEqual( dispatched_users, { @@ -1759,10 +1770,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 15 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 5, "User2": 10}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 5, "User2": 10}) self.assertDictEqual( dispatched_users, { @@ -1778,10 +1786,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 20 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 7, "User2": 13}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 7, "User2": 13}) self.assertDictEqual( dispatched_users, { @@ -1797,10 +1802,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 25 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 9, "User2": 16}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 9, "User2": 16}) self.assertDictEqual( dispatched_users, { @@ -1816,10 +1818,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 30 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 10, "User2": 20}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 10, "User2": 20}) self.assertDictEqual( dispatched_users, { @@ -1835,10 +1834,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 35 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 12, "User2": 23}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 12, "User2": 23}) self.assertDictEqual( dispatched_users, { @@ -1854,10 +1850,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 40 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 14, "User2": 26}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 14, "User2": 26}) self.assertDictEqual( dispatched_users, { @@ -1873,10 +1866,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 45 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 15, "User2": 30}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 15, "User2": 30}) self.assertDictEqual( dispatched_users, { @@ -1892,10 +1882,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 50 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 17, "User2": 33}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 17, "User2": 33}) self.assertDictEqual( dispatched_users, { @@ -1911,10 +1898,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 55 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 19, "User2": 36}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 19, "User2": 36}) self.assertDictEqual( dispatched_users, { @@ -1930,10 +1914,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 60 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 20, "User2": 40}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 20, "User2": 40}) self.assertDictEqual( dispatched_users, { @@ -1949,10 +1930,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 65 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 22, "User2": 43}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 22, "User2": 43}) self.assertDictEqual( dispatched_users, { @@ -1968,10 +1946,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 70 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 24, "User2": 46}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 24, "User2": 46}) self.assertDictEqual( dispatched_users, { @@ -1987,10 +1962,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): # total user count = 75, User1 = 25, User2 = 50 ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 25, "User2": 50}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 25, "User2": 50}) self.assertDictEqual( dispatched_users, { @@ -2031,21 +2003,14 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw next(worker_nodes_iterator).user_classes_count["User1"] += 1 user_count += 1 - users_dispatcher = dispatch_users( - worker_nodes=worker_nodes, - user_classes_count={"User1": 50}, - spawn_rate=1, - ) + users_dispatcher = UsersDispatcher(worker_nodes=worker_nodes, user_classes_count={"User1": 50}, spawn_rate=1) sleep_time = 1 for dispatch_iteration in range(25): ts = time.time() dispatched_users = next(users_dispatcher) - self.assertDictEqual( - _aggregate_dispatched_users(dispatched_users), - {"User1": 25 + dispatch_iteration + 1}, - ) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 25 + dispatch_iteration + 1}) delta = time.time() - ts if dispatch_iteration == 0: self.assertTrue(0 <= delta <= 0.02, delta) @@ -2058,43 +2023,6 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw self.assertTrue(0 <= delta <= 0.02, delta) -class TestNumberOfUsersLeftToDispatch(unittest.TestCase): - def test_number_of_users_left_to_dispatch(self): - user_classes_count = {"User1": 6, "User2": 2, "User3": 8} - balanced_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - - dispatched_users = { - "Worker1": {"User1": 5, "User2": 2, "User3": 6}, - "Worker2": {"User1": 5, "User2": 2, "User3": 6}, - } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) - self.assertEqual(0, result) - - dispatched_users = { - "Worker1": {"User1": 2, "User2": 0, "User3": 4}, - "Worker2": {"User1": 2, "User2": 0, "User3": 4}, - } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) - self.assertEqual(4, result) - - dispatched_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 0, "User3": 4}, - } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) - self.assertEqual(1, result) - - dispatched_users = { - "Worker1": {"User1": 3, "User2": 1, "User3": 4}, - "Worker2": {"User1": 3, "User2": 1, "User3": 4}, - } - result = number_of_users_left_to_dispatch(dispatched_users, balanced_users, user_classes_count) - self.assertEqual(0, result) - - def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: user_classes = list(next(iter(d.values())).keys()) return {u: sum(d[u] for d in d.values()) for u in user_classes} From 54ac967705a53b8a3918d2be19416356eb17e8be Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 16:01:05 -0400 Subject: [PATCH 096/139] Apply black with `--skip-magic-trailing-comma` on distribution code --- locust/distribution.py | 35 ++------ locust/test/test_distribution.py | 135 +++++++------------------------ 2 files changed, 34 insertions(+), 136 deletions(-) diff --git a/locust/distribution.py b/locust/distribution.py index 5ce9a033cc..1a59804c45 100644 --- a/locust/distribution.py +++ b/locust/distribution.py @@ -1,19 +1,12 @@ import math from itertools import combinations_with_replacement from operator import attrgetter -from typing import ( - Dict, - List, - Type, -) +from typing import Dict, List, Type from locust import User -def weight_users( - user_classes: List[Type[User]], - user_count: int, -) -> Dict[str, int]: +def weight_users(user_classes: List[Type[User]], user_count: int) -> Dict[str, int]: """ Compute the desired state of users using the weight of each user class. @@ -36,11 +29,7 @@ def weight_users( user_classes_count.update( { user_class.__name__: 1 - for user_class in sorted( - user_classes, - key=attrgetter("weight"), - reverse=True, - )[:user_count] + for user_class in sorted(user_classes, key=attrgetter("weight"), reverse=True)[:user_count] } ) return user_classes_count @@ -60,18 +49,14 @@ def weight_users( else: user_classes_count = _find_ideal_users_to_add_or_remove( - user_classes, - user_count - sum(user_classes_count.values()), - user_classes_count, + user_classes, user_count - sum(user_classes_count.values()), user_classes_count ) assert sum(user_classes_count.values()) == user_count return user_classes_count def _find_ideal_users_to_add_or_remove( - user_classes: List[Type[User]], - user_count_to_add_or_remove: int, - user_classes_count: Dict[str, int], + user_classes: List[Type[User]], user_count_to_add_or_remove: int, user_classes_count: Dict[str, int] ) -> Dict[str, int]: sign = -1 if user_count_to_add_or_remove < 0 else 1 @@ -97,10 +82,7 @@ def _find_ideal_users_to_add_or_remove( user_classes_count_candidate = user_classes_count.copy() for user_class in user_classes_combination: user_classes_count_candidate[user_class.__name__] += sign - distance = distance_from_desired_distribution( - user_classes, - user_classes_count_candidate, - ) + distance = distance_from_desired_distribution(user_classes, user_classes_count_candidate) if distance not in user_classes_count_candidates: user_classes_count_candidates[distance] = user_classes_count_candidate.copy() @@ -114,10 +96,7 @@ def _find_ideal_users_to_add_or_remove( return user_classes_count_candidate -def distance_from_desired_distribution( - user_classes: List[Type[User]], - user_classes_count: Dict[str, int], -) -> float: +def distance_from_desired_distribution(user_classes: List[Type[User]], user_classes_count: Dict[str, int]) -> float: actual_ratio_of_user_class = { user_class: user_class_count / sum(user_classes_count.values()) for user_class, user_class_count in user_classes_count.items() diff --git a/locust/test/test_distribution.py b/locust/test/test_distribution.py index 15d47030c6..5aaca73fd7 100644 --- a/locust/test/test_distribution.py +++ b/locust/test/test_distribution.py @@ -7,16 +7,10 @@ class TestDistribution(unittest.TestCase): def test_distribution_no_user_classes(self): - user_classes_count = weight_users( - user_classes=[], - user_count=0, - ) + user_classes_count = weight_users(user_classes=[], user_count=0) self.assertDictEqual(user_classes_count, {}) - user_classes_count = weight_users( - user_classes=[], - user_count=1, - ) + user_classes_count = weight_users(user_classes=[], user_count=1) self.assertDictEqual(user_classes_count, {}) def test_distribution_equal_weights_and_fewer_amount_than_user_classes(self): @@ -29,22 +23,13 @@ class User2(User): class User3(User): weight = 1 - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=0, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=0) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=1, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=1) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 0, "User3": 0}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=2, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=2) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 0}) def test_distribution_equal_weights(self): @@ -57,28 +42,16 @@ class User2(User): class User3(User): weight = 1 - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=3, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=3) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=4, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=4) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 1, "User3": 1}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=5, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=5) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=6, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=6) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 2, "User3": 2}) def test_distribution_unequal_and_unique_weights_and_fewer_amount_than_user_classes(self): @@ -91,22 +64,13 @@ class User2(User): class User3(User): weight = 3 - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=0, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=0) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=1, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=1) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 1}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=2, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=2) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 1}) def test_distribution_unequal_and_unique_weights(self): @@ -119,40 +83,22 @@ class User2(User): class User3(User): weight = 3 - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=3, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=3) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=4, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=4) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 2}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=5, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=5) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=6, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=6) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 3}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=10, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=10) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 3, "User3": 5}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=11, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=11) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 4, "User3": 5}) def test_distribution_unequal_and_non_unique_weights_and_fewer_amount_than_user_classes(self): @@ -165,22 +111,13 @@ class User2(User): class User3(User): weight = 2 - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=0, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=0) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 0, "User3": 0}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=1, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=1) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 0}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=2, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=2) self.assertDictEqual(user_classes_count, {"User1": 0, "User2": 1, "User3": 1}) def test_distribution_unequal_and_non_unique_weights(self): @@ -193,40 +130,22 @@ class User2(User): class User3(User): weight = 2 - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=3, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=3) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 1}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=4, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=4) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 1, "User3": 2}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=5, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=5) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 2, "User3": 2}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=6, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=6) self.assertDictEqual(user_classes_count, {"User1": 1, "User2": 3, "User3": 2}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=10, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=10) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 4, "User3": 4}) - user_classes_count = weight_users( - user_classes=[User1, User2, User3], - user_count=11, - ) + user_classes_count = weight_users(user_classes=[User1, User2, User3], user_count=11) self.assertDictEqual(user_classes_count, {"User1": 2, "User2": 5, "User3": 4}) def test_distribution_large_number_of_users(self): From 3323816ce54dbf1da521d6e3e69dd31090e21ebb Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 16:14:17 -0400 Subject: [PATCH 097/139] Remove `_user_count_left_to_dispatch` getter It is only used in `_all_users_have_been_dispatched` anyway --- locust/dispatch.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index b78ed62224..61cbbf4f9f 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -287,11 +287,8 @@ def _number_of_assigned_users_across_workers(assigned_users: Dict[str, Dict[str, @property def _all_users_have_been_dispatched(self) -> bool: - return self._user_count_left_to_dispatch == 0 - - @property - def _user_count_left_to_dispatch(self) -> int: - return sum(map(sum, map(dict.values, self._effective_assigned_users.values()))) + user_count_left_to_dispatch = sum(map(sum, map(dict.values, self._effective_assigned_users.values()))) + return user_count_left_to_dispatch == 0 def _try_next_user_class_to_stay_balanced_during_ramp_up(self, current_user_class: str) -> bool: """ From f8b480904ca34f75381902ffffd74da7e9ee38a3 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 16:21:26 -0400 Subject: [PATCH 098/139] Rename `dispatch_generator` to reduce confusion --- locust/dispatch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 61cbbf4f9f..a3a90b24e4 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -87,7 +87,8 @@ def __init__( # but in reverse. self._dispatched_users = deepcopy(self._initial_dispatched_users()) - self.__dispatch_generator = self._dispatch_generator() + # Initialize the generator that is used in `__next__` + self._dispatcher_generator = self._dispatcher() @functools.lru_cache() def _initial_dispatched_users(self) -> Dict[str, Dict[str, int]]: @@ -101,9 +102,9 @@ def _initial_dispatched_users(self) -> Dict[str, Dict[str, int]]: } def __next__(self) -> Dict[str, Dict[str, int]]: - return next(self.__dispatch_generator) + return next(self._dispatcher_generator) - def _dispatch_generator(self) -> Generator[Dict[str, Dict[str, int]], None, None]: + def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]: """Main iterator logic for dispatching users during a ramp-up""" if self._desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration: yield self._desired_users_assigned_to_workers() From eacd1e85776731a45f5886700472ca3644e1f6e4 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 16:42:02 -0400 Subject: [PATCH 099/139] Get rid of `_DistancesFromIdealDistribution` named tuple --- locust/dispatch.py | 52 ++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index a3a90b24e4..9f82c7f5cc 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -2,7 +2,6 @@ import itertools import math import time -from collections import namedtuple from copy import deepcopy from operator import itemgetter, methodcaller, ne from typing import Dict, Generator, List, TYPE_CHECKING @@ -14,11 +13,6 @@ from locust.runners import WorkerNode -_DistancesFromIdealDistribution = namedtuple( - "DistancesFromIdealDistribution", "actual_distance actual_distance_with_current_user_class" -) - - class UsersDispatcher(Iterator): """ Iterator that dispatches the users to the workers. @@ -340,8 +334,11 @@ def _dispatched_user_class_count(self, user_class: str) -> int: def _current_user_class_will_keep_distribution_better_than_all_other_user_classes( self, current_user_class: str ) -> bool: - distances = self._distances_from_ideal_distribution(current_user_class) - if distances.actual_distance_with_current_user_class > distances.actual_distance and all( + actual_distance = self._actual_distance_from_ideal_distribution() + actual_distance_with_current_user_class = self._actual_distance_from_ideal_distribution_with_current_user_class( + current_user_class + ) + if actual_distance_with_current_user_class > actual_distance and all( not self._current_user_class_will_keep_distribution(user_class) for user_class in self._user_classes_count.keys() if user_class != current_user_class @@ -350,28 +347,31 @@ def _current_user_class_will_keep_distribution_better_than_all_other_user_classe # then the distribution will be the best we can get. In other words, adding # one user of any other user class won't yield a better distribution. return True - if distances.actual_distance_with_current_user_class <= distances.actual_distance: + if actual_distance_with_current_user_class <= actual_distance: return True return False def _current_user_class_will_keep_distribution(self, current_user_class: str) -> bool: - distances = self._distances_from_ideal_distribution(current_user_class) - if distances.actual_distance_with_current_user_class <= distances.actual_distance: + if ( + self._actual_distance_from_ideal_distribution_with_current_user_class(current_user_class) + <= self._actual_distance_from_ideal_distribution() + ): return True return False - def _distances_from_ideal_distribution(self, current_user_class) -> _DistancesFromIdealDistribution: - user_classes = list(self._user_classes_count.keys()) - - desired_weights = [ - self._user_classes_count[user_class] / sum(self._user_classes_count.values()) for user_class in user_classes - ] + def _actual_distance_from_ideal_distribution(self) -> float: + user_classes = sorted(self._user_classes_count.keys()) actual_weights = [ self._dispatched_user_classes_count()[user_class] / sum(self._dispatched_user_classes_count().values()) for user_class in user_classes ] + return math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, self._desired_weights())))) + + def _actual_distance_from_ideal_distribution_with_current_user_class(self, current_user_class: str) -> float: + user_classes = sorted(self._user_classes_count.keys()) + actual_weights_with_current_user_class = [ ( self._dispatched_user_classes_count()[user_class] + 1 @@ -382,18 +382,16 @@ def _distances_from_ideal_distribution(self, current_user_class) -> _DistancesFr for user_class in user_classes ] - actual_distance = math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, desired_weights)))) - - actual_distance_with_current_user_class = math.sqrt( - sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights_with_current_user_class, desired_weights))) + return math.sqrt( + sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights_with_current_user_class, self._desired_weights()))) ) - # `actual_distance` corresponds to the distance from the ideal distribution given the - # users dispatched at this time. `actual_distance_with_current_user_class` represents the distance - # from the ideal distribution if we were to add one user of the given `current_user_class`. - # Thus, we want to find the best user class, in which to add a user, that will give us - # an `actual_distance_with_current_user_class` less than `actual_distance`. - return _DistancesFromIdealDistribution(actual_distance, actual_distance_with_current_user_class) + @functools.lru_cache() + def _desired_weights(self) -> List[float]: + return [ + self._user_classes_count[user_class] / sum(self._user_classes_count.values()) + for user_class in sorted(self._user_classes_count.keys()) + ] @functools.lru_cache() def _dispatched_user_classes_count(self) -> Dict[str, int]: From 10004da31c4cd7167fce730ed49ba833a0942e32 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 16:45:38 -0400 Subject: [PATCH 100/139] Sort user classes once --- locust/dispatch.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 9f82c7f5cc..07b7497dff 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -52,6 +52,8 @@ def __init__( self._user_classes_count = user_classes_count + self._sorted_user_classes = sorted(user_classes_count.keys()) + self._spawn_rate = spawn_rate # This represents the desired users distribution minus the already running users among the workers. @@ -153,7 +155,7 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: user_count_in_current_dispatch = 0 - for i, current_user_class in enumerate(itertools.cycle(sorted(self._user_classes_count.keys()))): + for i, current_user_class in enumerate(itertools.cycle(self._sorted_user_classes)): # For large number of user classes and large number of workers, this assertion might fail. # If this happens, you can remove it or increase the threshold. Right now, the assertion # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). @@ -227,7 +229,7 @@ def _desired_users_assigned_to_workers(self) -> Dict[str, Dict[str, int]]: the same number of users of each user class """ assigned_users = { - worker_node.id: {user_class: 0 for user_class in sorted(self._user_classes_count.keys())} + worker_node.id: {user_class: 0 for user_class in self._sorted_user_classes} for worker_node in self._worker_nodes } @@ -239,7 +241,7 @@ def _desired_users_assigned_to_workers(self) -> Dict[str, Dict[str, int]]: # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. users_per_worker, remainder = divmod(user_count, len(self._worker_nodes)) - for user_class in sorted(user_classes_count.keys()): + for user_class in self._sorted_user_classes: if sum(user_classes_count.values()) == 0: # No more users of any user class to assign to workers, so we can exit this loop. break @@ -310,9 +312,7 @@ def _try_next_user_class_to_stay_balanced_during_ramp_up(self, current_user_clas else: # Because each user class doesn't have at least one running user, we use a simpler strategy # that make sure each user class appears once. - for next_user_class in filter( - functools.partial(ne, current_user_class), sorted(self._user_classes_count.keys()) - ): + for next_user_class in filter(functools.partial(ne, current_user_class), self._sorted_user_classes): # TODO: Put in function `user_class_count_left_to_dispatch` if sum(map(itemgetter(next_user_class), self._effective_assigned_users.values())) == 0: # No more users of class `next_user_class` to dispatch @@ -360,18 +360,14 @@ def _current_user_class_will_keep_distribution(self, current_user_class: str) -> return False def _actual_distance_from_ideal_distribution(self) -> float: - user_classes = sorted(self._user_classes_count.keys()) - actual_weights = [ self._dispatched_user_classes_count()[user_class] / sum(self._dispatched_user_classes_count().values()) - for user_class in user_classes + for user_class in self._sorted_user_classes ] return math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, self._desired_weights())))) def _actual_distance_from_ideal_distribution_with_current_user_class(self, current_user_class: str) -> float: - user_classes = sorted(self._user_classes_count.keys()) - actual_weights_with_current_user_class = [ ( self._dispatched_user_classes_count()[user_class] + 1 @@ -379,7 +375,7 @@ def _actual_distance_from_ideal_distribution_with_current_user_class(self, curre else self._dispatched_user_classes_count()[user_class] ) / (sum(self._dispatched_user_classes_count().values()) + 1) - for user_class in user_classes + for user_class in self._sorted_user_classes ] return math.sqrt( @@ -390,7 +386,7 @@ def _actual_distance_from_ideal_distribution_with_current_user_class(self, curre def _desired_weights(self) -> List[float]: return [ self._user_classes_count[user_class] / sum(self._user_classes_count.values()) - for user_class in sorted(self._user_classes_count.keys()) + for user_class in self._sorted_user_classes ] @functools.lru_cache() From 539d44e1588195f266e59b1cde03d74c941c5dbc Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 16:51:36 -0400 Subject: [PATCH 101/139] Simplify code by removing if conditional --- locust/dispatch.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 07b7497dff..8906392229 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -347,9 +347,7 @@ def _current_user_class_will_keep_distribution_better_than_all_other_user_classe # then the distribution will be the best we can get. In other words, adding # one user of any other user class won't yield a better distribution. return True - if actual_distance_with_current_user_class <= actual_distance: - return True - return False + return actual_distance_with_current_user_class <= actual_distance def _current_user_class_will_keep_distribution(self, current_user_class: str) -> bool: if ( From a86698ea71932644faa8011f0c30a50513c161c2 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 22 Jun 2021 17:20:52 -0400 Subject: [PATCH 102/139] Refactor naming of some methods and variables in dispatch module --- locust/dispatch.py | 76 +++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 8906392229..e9e8e4ebaf 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -155,7 +155,7 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: user_count_in_current_dispatch = 0 - for i, current_user_class in enumerate(itertools.cycle(self._sorted_user_classes)): + for i, user_class_to_add in enumerate(itertools.cycle(self._sorted_user_classes)): # For large number of user classes and large number of workers, this assertion might fail. # If this happens, you can remove it or increase the threshold. Right now, the assertion # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). @@ -170,23 +170,20 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: ): break - if ( - self._dispatched_user_class_count(current_user_class) - >= self._user_classes_count[current_user_class] - ): + if self._dispatched_user_class_count(user_class_to_add) >= self._user_classes_count[user_class_to_add]: continue - if self._try_next_user_class_to_stay_balanced_during_ramp_up(current_user_class): + if self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up(user_class_to_add): continue for j, worker_node_id in enumerate(itertools.cycle(sorted(self._effective_assigned_users.keys()))): assert j < int( 2 * self._number_of_workers ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if self._effective_assigned_users[worker_node_id][current_user_class] == 0: + if self._effective_assigned_users[worker_node_id][user_class_to_add] == 0: continue - self._dispatched_users[worker_node_id][current_user_class] += 1 - self._effective_assigned_users[worker_node_id][current_user_class] -= 1 + self._dispatched_users[worker_node_id][user_class_to_add] += 1 + self._effective_assigned_users[worker_node_id][user_class_to_add] -= 1 user_count_in_current_dispatch += 1 break @@ -287,7 +284,7 @@ def _all_users_have_been_dispatched(self) -> bool: user_count_left_to_dispatch = sum(map(sum, map(dict.values, self._effective_assigned_users.values()))) return user_count_left_to_dispatch == 0 - def _try_next_user_class_to_stay_balanced_during_ramp_up(self, current_user_class: str) -> bool: + def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_class_to_add: str) -> bool: """ Whether to skip to next user class or not. This is done so that the distribution of user class stays approximately balanced during @@ -295,15 +292,15 @@ def _try_next_user_class_to_stay_balanced_during_ramp_up(self, current_user_clas """ # For performance reasons, we use `functools.lru_cache()` on the `self._dispatched_user_classes_count` # method because its value does not change within the scope of the current method. However, the next time - # `self._try_next_user_class_to_stay_balanced_during_ramp_up` is invoked, we need + # `self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up` is invoked, we need # `self._dispatched_user_classes_count` to be recomputed. self._dispatched_user_classes_count.cache_clear() if all(user_count > 0 for user_count in self._dispatched_user_classes_count().values()): # We're here because each user class have at least one user running. Thus, # we need to ensure that the distribution of users corresponds to the weights. - if not self._current_user_class_will_keep_distribution_better_than_all_other_user_classes( - current_user_class + if not self._adding_this_user_class_respects_distribution_better_than_adding_any_other_user_class( + user_class_to_add ): return True else: @@ -312,18 +309,17 @@ def _try_next_user_class_to_stay_balanced_during_ramp_up(self, current_user_clas else: # Because each user class doesn't have at least one running user, we use a simpler strategy # that make sure each user class appears once. - for next_user_class in filter(functools.partial(ne, current_user_class), self._sorted_user_classes): - # TODO: Put in function `user_class_count_left_to_dispatch` + for next_user_class in filter(functools.partial(ne, user_class_to_add), self._sorted_user_classes): if sum(map(itemgetter(next_user_class), self._effective_assigned_users.values())) == 0: # No more users of class `next_user_class` to dispatch continue if ( - self._dispatched_user_classes_count()[current_user_class] + self._dispatched_user_classes_count()[user_class_to_add] - self._dispatched_user_classes_count()[next_user_class] >= 1 ): - # There's already enough users for `current_user_class` in the current dispatch. Hence, we should - # not consider `current_user_class` and go to the next user class instead. + # There's already enough users for `user_class_to_add` in the current dispatch. Hence, we should + # not consider `user_class_to_add` and go to the next user class instead. return True return False @@ -331,45 +327,49 @@ def _dispatched_user_class_count(self, user_class: str) -> int: """Number of dispatched users for the given user class""" return sum(map(itemgetter(user_class), self._dispatched_users.values())) - def _current_user_class_will_keep_distribution_better_than_all_other_user_classes( - self, current_user_class: str + def _adding_this_user_class_respects_distribution_better_than_adding_any_other_user_class( + self, user_class_to_add: str ) -> bool: - actual_distance = self._actual_distance_from_ideal_distribution() - actual_distance_with_current_user_class = self._actual_distance_from_ideal_distribution_with_current_user_class( - current_user_class + distance = self._distance_from_ideal_distribution() + distance_after_adding_user_class = self._distance_from_ideal_distribution_after_adding_this_user_class( + user_class_to_add ) - if actual_distance_with_current_user_class > actual_distance and all( - not self._current_user_class_will_keep_distribution(user_class) + if distance_after_adding_user_class > distance and all( + not self._adding_this_user_class_respects_distribution(user_class) for user_class in self._user_classes_count.keys() - if user_class != current_user_class + if user_class != user_class_to_add ): - # If we are here, it means that if one user of `current_user_class` is added + # If we are here, it means that if one user of `user_class_to_add` is added # then the distribution will be the best we can get. In other words, adding # one user of any other user class won't yield a better distribution. return True - return actual_distance_with_current_user_class <= actual_distance + return distance_after_adding_user_class <= distance - def _current_user_class_will_keep_distribution(self, current_user_class: str) -> bool: + def _adding_this_user_class_respects_distribution(self, user_class_to_add: str) -> bool: if ( - self._actual_distance_from_ideal_distribution_with_current_user_class(current_user_class) - <= self._actual_distance_from_ideal_distribution() + self._distance_from_ideal_distribution_after_adding_this_user_class(user_class_to_add) + <= self._distance_from_ideal_distribution() ): return True return False - def _actual_distance_from_ideal_distribution(self) -> float: - actual_weights = [ + def _distance_from_ideal_distribution(self) -> float: + """How far are we from the ideal distribution given the current set of running users?""" + weights = [ self._dispatched_user_classes_count()[user_class] / sum(self._dispatched_user_classes_count().values()) for user_class in self._sorted_user_classes ] - return math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights, self._desired_weights())))) + return math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(weights, self._desired_weights())))) - def _actual_distance_from_ideal_distribution_with_current_user_class(self, current_user_class: str) -> float: - actual_weights_with_current_user_class = [ + def _distance_from_ideal_distribution_after_adding_this_user_class(self, user_class_to_add: str) -> float: + """ + How far are we from the ideal distribution if we were to add `user_class_to_add` to the pool of running users? + """ + weights_with_added_user_class = [ ( self._dispatched_user_classes_count()[user_class] + 1 - if user_class == current_user_class + if user_class == user_class_to_add else self._dispatched_user_classes_count()[user_class] ) / (sum(self._dispatched_user_classes_count().values()) + 1) @@ -377,7 +377,7 @@ def _actual_distance_from_ideal_distribution_with_current_user_class(self, curre ] return math.sqrt( - sum(map(lambda x: (x[1] - x[0]) ** 2, zip(actual_weights_with_current_user_class, self._desired_weights()))) + sum(map(lambda x: (x[1] - x[0]) ** 2, zip(weights_with_added_user_class, self._desired_weights()))) ) @functools.lru_cache() From 031e529a7ced76e9cae3596e389cdb02be1f5ac2 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 23 Jun 2021 11:47:53 -0400 Subject: [PATCH 103/139] Add docstring and small refactoring for dispatch code Hopefully, this makes the code easier to follow and understand. The logic for assigning the users to each worker has been relocated in its own class `WorkersUsersAssignor` because this logic was not coupled with the `UsersDispatcher`. --- locust/dispatch.py | 224 ++++++++++++++++++++--------------- locust/test/test_dispatch.py | 36 +++--- 2 files changed, 148 insertions(+), 112 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index e9e8e4ebaf..b289efe3f2 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -2,12 +2,12 @@ import itertools import math import time +from collections.abc import Iterator from copy import deepcopy from operator import itemgetter, methodcaller, ne from typing import Dict, Generator, List, TYPE_CHECKING import gevent -from collections.abc import Iterator if TYPE_CHECKING: from locust.runners import WorkerNode @@ -31,14 +31,23 @@ class UsersDispatcher(Iterator): a stop timeout is specified. When a stop timeout is specified combined with users having long-running tasks, attempting to stop the users at a spawn rate will lead to weird behaviours (users being killed even though the stop timeout is not reached yet). + + The terminology used in the users dispatcher is: + - Dispatch cycle + A dispatch cycle corresponds to a ramp-up from start to finish. So, + going from 10 to 100 users with a spawn rate of 1/s corresponds to one + dispatch cycle. An instance of the `UsersDispatcher` class "lives" for + one dispatch cycle only. + - Dispatch iteration + A dispatch cycle contains one or more dispatch iterations. In the previous example + of going from 10 to 100 users with a spawn rate of 1/s, there are 100 dispatch iterations. + That is, from 10 to 11 users is a dispatch iteration, from 12 to 13 is another, and so on. + If the spawn rate were to be 2/s, then there would be 50 dispatch iterations for this dispatch cycle. + For a more extreme case with a spawn rate of 120/s, there would be only a single dispatch iteration + from 10 to 100. """ - def __init__( - self, - worker_nodes, # type: List[WorkerNode] - user_classes_count: Dict[str, int], - spawn_rate: float, - ): + def __init__(self, worker_nodes: "List[WorkerNode]", user_classes_count: Dict[str, int], spawn_rate: float): """ :param worker_nodes: List of worker nodes :param user_classes_count: Desired number of users for each class @@ -52,20 +61,24 @@ def __init__( self._user_classes_count = user_classes_count - self._sorted_user_classes = sorted(user_classes_count.keys()) + self._user_classes = sorted(user_classes_count.keys()) self._spawn_rate = spawn_rate + self._desired_users_assigned_to_workers = _WorkersUsersAssignor( + user_classes_count, worker_nodes + ).desired_users_assigned_to_workers + # This represents the desired users distribution minus the already running users among the workers. # The values inside this dictionary are updated during the current dispatch cycle. For example, # if we dispatch 1 user of UserClass1 to worker 1, then we will decrement by 1 the user count # for UserClass1 of worker 1. Naturally, the current dispatch cycle is done once all the values # reach zero. - self._effective_assigned_users = { + self._users_left_to_assigned = { worker_node.id: { user_class: max( 0, - self._desired_users_assigned_to_workers()[worker_node.id][user_class] + self._desired_users_assigned_to_workers[worker_node.id][user_class] - self._initial_dispatched_users()[worker_node.id][user_class], ) for user_class in self._user_classes_count.keys() @@ -101,9 +114,9 @@ def __next__(self) -> Dict[str, Dict[str, int]]: return next(self._dispatcher_generator) def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]: - """Main iterator logic for dispatching users during a ramp-up""" + """Main iterator logic for dispatching users during this dispatch cycle""" if self._desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration: - yield self._desired_users_assigned_to_workers() + yield self._desired_users_assigned_to_workers else: while not self._all_users_have_been_dispatched: @@ -122,7 +135,7 @@ def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(sel user_count_left_to_dispatch_excluding_excess_users = sum( max( 0, - sum(map(itemgetter(user_class), self._desired_users_assigned_to_workers().values())) + sum(map(itemgetter(user_class), self._desired_users_assigned_to_workers.values())) - self._dispatched_user_class_count(user_class), ) for user_class in self._user_classes_count.keys() @@ -142,8 +155,8 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: # User count for every user class is greater than or equal to the target user count of each class. # This means that we're at the last iteration of this dispatch cycle. If some user classes are in # excess, this last iteration will stop those excess users. - self._dispatched_users.update(self._desired_users_assigned_to_workers()) - self._effective_assigned_users.update( + self._dispatched_users.update(self._desired_users_assigned_to_workers) + self._users_left_to_assigned.update( { worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} for worker_node_id, user_classes_count in self._dispatched_users.items() @@ -155,7 +168,7 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: user_count_in_current_dispatch = 0 - for i, user_class_to_add in enumerate(itertools.cycle(self._sorted_user_classes)): + for i, user_class_to_add in enumerate(itertools.cycle(self._user_classes)): # For large number of user classes and large number of workers, this assertion might fail. # If this happens, you can remove it or increase the threshold. Right now, the assertion # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). @@ -176,14 +189,14 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: if self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up(user_class_to_add): continue - for j, worker_node_id in enumerate(itertools.cycle(sorted(self._effective_assigned_users.keys()))): + for j, worker_node_id in enumerate(itertools.cycle(sorted(self._users_left_to_assigned.keys()))): assert j < int( 2 * self._number_of_workers ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if self._effective_assigned_users[worker_node_id][user_class_to_add] == 0: + if self._users_left_to_assigned[worker_node_id][user_class_to_add] == 0: continue self._dispatched_users[worker_node_id][user_class_to_add] += 1 - self._effective_assigned_users[worker_node_id][user_class_to_add] -= 1 + self._users_left_to_assigned[worker_node_id][user_class_to_add] -= 1 user_count_in_current_dispatch += 1 break @@ -217,78 +230,18 @@ def _assert_computation_duration_of_dispatch_is_reasonable(self, duration: float @property def _number_of_workers(self) -> int: - return len(self._effective_assigned_users) - - @functools.lru_cache() - def _desired_users_assigned_to_workers(self) -> Dict[str, Dict[str, int]]: - """ - Assign users to workers so that each worker gets around - the same number of users of each user class - """ - assigned_users = { - worker_node.id: {user_class: 0 for user_class in self._sorted_user_classes} - for worker_node in self._worker_nodes - } - - # We need to copy to prevent modifying `user_classes_count`. - user_classes_count = self._user_classes_count.copy() - - user_count = sum(user_classes_count.values()) - - # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. - users_per_worker, remainder = divmod(user_count, len(self._worker_nodes)) - - for user_class in self._sorted_user_classes: - if sum(user_classes_count.values()) == 0: - # No more users of any user class to assign to workers, so we can exit this loop. - break - - # Assign users of `user_class` to the workers in a round-robin fashion. - for worker_node in itertools.cycle(self._worker_nodes): - if user_classes_count[user_class] == 0: - break - - number_of_users_left_to_assign = user_count - self._number_of_assigned_users_across_workers( - assigned_users - ) - - if ( - self._number_of_assigned_users_for_worker(assigned_users, worker_node) == users_per_worker - and number_of_users_left_to_assign > remainder - ): - continue - - elif ( - self._number_of_assigned_users_for_worker(assigned_users, worker_node) == users_per_worker + 1 - and number_of_users_left_to_assign < remainder - ): - continue - - assigned_users[worker_node.id][user_class] += 1 - user_classes_count[user_class] -= 1 - - return assigned_users - - @staticmethod - def _number_of_assigned_users_for_worker( - assigned_users: Dict[str, Dict[str, int]], worker_node # type: WorkerNode - ) -> int: - return sum(assigned_users[worker_node.id].values()) - - @staticmethod - def _number_of_assigned_users_across_workers(assigned_users: Dict[str, Dict[str, int]]) -> int: - return sum(map(sum, map(methodcaller("values"), assigned_users.values()))) + return len(self._users_left_to_assigned) @property def _all_users_have_been_dispatched(self) -> bool: - user_count_left_to_dispatch = sum(map(sum, map(dict.values, self._effective_assigned_users.values()))) + user_count_left_to_dispatch = sum(map(sum, map(dict.values, self._users_left_to_assigned.values()))) return user_count_left_to_dispatch == 0 def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_class_to_add: str) -> bool: """ - Whether to skip to next user class or not. This is done so that - the distribution of user class stays approximately balanced during - a ramp up. + Whether to skip to the next user class or not. This is done so that + the distribution of user class stays approximately balanced from one dispatch + iteration to another. """ # For performance reasons, we use `functools.lru_cache()` on the `self._dispatched_user_classes_count` # method because its value does not change within the scope of the current method. However, the next time @@ -309,8 +262,8 @@ def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_cla else: # Because each user class doesn't have at least one running user, we use a simpler strategy # that make sure each user class appears once. - for next_user_class in filter(functools.partial(ne, user_class_to_add), self._sorted_user_classes): - if sum(map(itemgetter(next_user_class), self._effective_assigned_users.values())) == 0: + for next_user_class in filter(functools.partial(ne, user_class_to_add), self._user_classes): + if sum(map(itemgetter(next_user_class), self._users_left_to_assigned.values())) == 0: # No more users of class `next_user_class` to dispatch continue if ( @@ -324,7 +277,7 @@ def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_cla return False def _dispatched_user_class_count(self, user_class: str) -> int: - """Number of dispatched users for the given user class""" + """Number of dispatched users at this time for the given user class""" return sum(map(itemgetter(user_class), self._dispatched_users.values())) def _adding_this_user_class_respects_distribution_better_than_adding_any_other_user_class( @@ -357,38 +310,121 @@ def _distance_from_ideal_distribution(self) -> float: """How far are we from the ideal distribution given the current set of running users?""" weights = [ self._dispatched_user_classes_count()[user_class] / sum(self._dispatched_user_classes_count().values()) - for user_class in self._sorted_user_classes + for user_class in self._user_classes ] - return math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(weights, self._desired_weights())))) + return math.sqrt(sum(map(lambda x: (x[1] - x[0]) ** 2, zip(weights, self._desired_relative_weights())))) def _distance_from_ideal_distribution_after_adding_this_user_class(self, user_class_to_add: str) -> float: """ How far are we from the ideal distribution if we were to add `user_class_to_add` to the pool of running users? """ - weights_with_added_user_class = [ + relative_weights_with_added_user_class = [ ( self._dispatched_user_classes_count()[user_class] + 1 if user_class == user_class_to_add else self._dispatched_user_classes_count()[user_class] ) / (sum(self._dispatched_user_classes_count().values()) + 1) - for user_class in self._sorted_user_classes + for user_class in self._user_classes ] return math.sqrt( - sum(map(lambda x: (x[1] - x[0]) ** 2, zip(weights_with_added_user_class, self._desired_weights()))) + sum( + map( + lambda x: (x[1] - x[0]) ** 2, + zip(relative_weights_with_added_user_class, self._desired_relative_weights()), + ) + ) ) @functools.lru_cache() - def _desired_weights(self) -> List[float]: + def _desired_relative_weights(self) -> List[float]: + """The relative weight of each user class we desire""" return [ self._user_classes_count[user_class] / sum(self._user_classes_count.values()) - for user_class in self._sorted_user_classes + for user_class in self._user_classes ] @functools.lru_cache() def _dispatched_user_classes_count(self) -> Dict[str, int]: + """The user count for each user class that are dispatched at this time""" return { user_class: self._dispatched_user_class_count(user_class) for user_class in self._user_classes_count.keys() } + + +class _WorkersUsersAssignor: + """Helper to compute the users assigned to the workers""" + + def __init__(self, user_classes_count: Dict[str, int], worker_nodes: "List[WorkerNode]"): + self._user_classes_count = user_classes_count + self._user_classes = sorted(user_classes_count.keys()) + self._worker_nodes = sorted(worker_nodes, key=lambda w: w.id) + + @property + def desired_users_assigned_to_workers(self) -> Dict[str, Dict[str, int]]: + """The users assigned to the workers. + + The assignment is done in a way that each worker gets around the same number of users of each user class. + If some user classes are more represented than others, then it is not possible to equally distribute + the users from each user class to all workers. It is done in a best-effort. + + The assignment also ensures that each worker runs the same amount of users (irrespective of the user class + of those users). If the total user count does not yield an integer when divided by the number of workers, + then some workers will have one more user than the others. + """ + assigned_users = { + worker_node.id: {user_class: 0 for user_class in self._user_classes} for worker_node in self._worker_nodes + } + + # We need to copy to prevent modifying `user_classes_count`. + user_classes_count = self._user_classes_count.copy() + + user_count = sum(user_classes_count.values()) + + # If `remainder > 0`, it means that some workers will have `users_per_worker + 1` users. + users_per_worker, remainder = divmod(user_count, len(self._worker_nodes)) + + for user_class in self._user_classes: + if sum(user_classes_count.values()) == 0: + # No more users of any user class to assign to workers, so we can exit this loop. + break + + # Assign users of `user_class` to the workers in a round-robin fashion. + for worker_node in itertools.cycle(self._worker_nodes): + if user_classes_count[user_class] == 0: + break + + number_of_users_left_to_assign = user_count - self._number_of_assigned_users_across_workers( + assigned_users + ) + + if ( + self._number_of_assigned_users_for_worker(assigned_users, worker_node) == users_per_worker + and number_of_users_left_to_assign > remainder + ): + continue + + elif ( + self._number_of_assigned_users_for_worker(assigned_users, worker_node) == users_per_worker + 1 + and number_of_users_left_to_assign < remainder + ): + continue + + assigned_users[worker_node.id][user_class] += 1 + user_classes_count[user_class] -= 1 + + return assigned_users + + @staticmethod + def _number_of_assigned_users_for_worker( + assigned_users: Dict[str, Dict[str, int]], worker_node: "WorkerNode" + ) -> int: + """User count running on the given worker""" + return sum(assigned_users[worker_node.id].values()) + + @staticmethod + def _number_of_assigned_users_across_workers(assigned_users: Dict[str, Dict[str, int]]) -> int: + """Total user count running on the workers""" + return sum(map(sum, map(methodcaller("values"), assigned_users.values()))) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 5f872b8f10..2848483e9d 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -15,35 +15,35 @@ def test_assign_users_to_1_worker(self): worker_nodes=[worker_node1], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, spawn_rate=1 ) self.assertDictEqual( - users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 3, "User2": 3, "User3": 3}} + users_dispatcher._desired_users_assigned_to_workers, {"1": {"User1": 3, "User2": 3, "User3": 3}} ) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1], user_classes_count={"User1": 5, "User2": 4, "User3": 2}, spawn_rate=1 ) self.assertDictEqual( - users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 5, "User2": 4, "User3": 2}} + users_dispatcher._desired_users_assigned_to_workers, {"1": {"User1": 5, "User2": 4, "User3": 2}} ) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1], user_classes_count={"User1": 1, "User2": 1, "User3": 1}, spawn_rate=1 ) self.assertDictEqual( - users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 1, "User2": 1, "User3": 1}} + users_dispatcher._desired_users_assigned_to_workers, {"1": {"User1": 1, "User2": 1, "User3": 1}} ) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1], user_classes_count={"User1": 1, "User2": 1, "User3": 0}, spawn_rate=1 ) self.assertDictEqual( - users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 1, "User2": 1, "User3": 0}} + users_dispatcher._desired_users_assigned_to_workers, {"1": {"User1": 1, "User2": 1, "User3": 0}} ) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1], user_classes_count={"User1": 0, "User2": 0, "User3": 0}, spawn_rate=1 ) self.assertDictEqual( - users_dispatcher._desired_users_assigned_to_workers(), {"1": {"User1": 0, "User2": 0, "User3": 0}} + users_dispatcher._desired_users_assigned_to_workers, {"1": {"User1": 0, "User2": 0, "User3": 0}} ) def test_assign_users_to_3_workers(self): @@ -61,7 +61,7 @@ def test_assign_users_to_3_workers(self): "2": {"User1": 1, "User2": 1, "User3": 1}, "3": {"User1": 1, "User2": 1, "User3": 1}, } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -73,7 +73,7 @@ def test_assign_users_to_3_workers(self): "2": {"User1": 2, "User2": 1, "User3": 1}, "3": {"User1": 1, "User2": 2, "User3": 0}, } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -85,7 +85,7 @@ def test_assign_users_to_3_workers(self): "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 1}, } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -97,7 +97,7 @@ def test_assign_users_to_3_workers(self): "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -109,7 +109,7 @@ def test_assign_users_to_3_workers(self): "2": {"User1": 0, "User2": 0, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) def test_assign_5_users_to_10_workers(self): # Prepend "0" to worker name under 10, because workers are sorted in alphabetical orders. @@ -134,7 +134,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -153,7 +153,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -172,7 +172,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -191,7 +191,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -210,7 +210,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -229,7 +229,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -248,7 +248,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 2, "User5": 0}, # 3 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 1, "User5": 1}, # 3 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) users_dispatcher = UsersDispatcher( worker_nodes=worker_nodes, @@ -267,7 +267,7 @@ def test_assign_5_users_to_10_workers(self): "09": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users "10": {"User1": 1, "User2": 0, "User3": 0, "User4": 0, "User5": 3}, # 4 users } - self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers(), expected_balanced_users) + self.assertDictEqual(users_dispatcher._desired_users_assigned_to_workers, expected_balanced_users) class TestDispatchUsersWithWorkersWithoutPriorUsers(unittest.TestCase): From 7fed917e2283178ec3e4b645077478351d162f16 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 23 Jun 2021 12:46:33 -0400 Subject: [PATCH 104/139] Remove `_assert_computation_duration_of_dispatch_is_reasonable` It should not happen in real life and highly depends on the host, the current cpu load, the number of workers, users and user classes. If there are performance issues, people will probably raise issue anyway. Then, we can run profiling. --- locust/dispatch.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index b289efe3f2..5e175c7545 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -203,31 +203,11 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: if user_count_in_current_dispatch == self._user_count_per_dispatch: break - self._assert_computation_duration_of_dispatch_is_reasonable(duration=time.perf_counter() - ts_dispatch) - return { worker_node_id: dict(sorted(user_classes_count.items(), key=itemgetter(0))) for worker_node_id, user_classes_count in sorted(self._dispatched_users.items(), key=itemgetter(0)) } - def _assert_computation_duration_of_dispatch_is_reasonable(self, duration: float) -> None: - # Safeguard against unforeseen performance issues. Ideally, - # we want each dispatch loop to be as short as possible to compute, but with - # a large amount of workers/user classes, it can take longer to come up with the dispatch solution. - # If the assertion is raised, then it could be a sign that the code needs to be optimized for the - # situation that caused the assertion to be raised. - assert duration < ( - 0.5 - if self._number_of_workers < 100 - else 1 - if self._number_of_workers < 250 - else 1.5 - if self._number_of_workers < 350 - else 3 - ), "Dispatch iteration took too much time: {}s (len(workers) = {}, len(user_classes) = {})".format( - duration, self._number_of_workers, len(self._user_classes_count) - ) - @property def _number_of_workers(self) -> int: return len(self._users_left_to_assigned) From 182918d27891c5a277333df41e16ed8ebecde487 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 23 Jun 2021 12:51:56 -0400 Subject: [PATCH 105/139] Remove unused variable --- locust/dispatch.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 5e175c7545..cc2138d6a0 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -164,8 +164,6 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: ) else: - ts_dispatch = time.perf_counter() - user_count_in_current_dispatch = 0 for i, user_class_to_add in enumerate(itertools.cycle(self._user_classes)): From 2b306b26f62db4efc711877eda2f3a67a7e05ed3 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 23 Jun 2021 16:05:30 -0400 Subject: [PATCH 106/139] Fix indentation + remove space on empty line --- locust/test/test_runners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 09f8c74925..b88245fc8c 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1720,7 +1720,7 @@ def my_task(self): self.assertEqual(1, master.target_user_count) self.assertEqual(3, master.spawn_rate) - def test_custom_message_send(self): + def test_custom_message_send(self): class MyUser(User): wait_time = constant(1) @@ -1800,7 +1800,7 @@ def _patch_env(name: str, value: str): else: os.environ[name] = prev_value - + class TestWorkerRunner(LocustTestCase): def setUp(self): super().setUp() From 1df84d6c5eb8b1c5193324602cbac21622b6841c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Wed, 23 Jun 2021 16:05:45 -0400 Subject: [PATCH 107/139] Cast worker node name to string in test --- locust/test/test_runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index b88245fc8c..842cb4b052 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1731,7 +1731,7 @@ def my_task(self): with mock.patch("locust.rpc.rpc.Server", mocked_rpc()) as server: master = self.get_runner() for i in range(5): - master.clients[i] = WorkerNode(i) + master.clients[i] = WorkerNode(str(i)) master.send_message("test_custom_msg", {"test_data": 123}) self.assertEqual(5, len(server.outbox)) From 611bdc01db3d552667806aabc3a2fbc479175fda Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 24 Jun 2021 18:06:02 -0400 Subject: [PATCH 108/139] Ensure master's state is `spawning` during ramp-up --- locust/runners.py | 6 +-- locust/test/test_runners.py | 79 ++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 338678aeac..6998b9694e 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -709,6 +709,8 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: finally: timeout.cancel() + self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_classes_count.values())) + logger.info( "All users spawned: %s (%i total running)" % ( @@ -842,10 +844,6 @@ def client_listener(self): elif msg.type == "spawning_complete": self.clients[msg.node_id].state = STATE_RUNNING self.clients[msg.node_id].user_classes_count = msg.data["user_classes_count"] - if len(self.clients.spawning) == 0: - self.environment.events.spawning_complete.fire( - user_count=sum(self.target_user_classes_count.values()) - ) elif msg.type == "quit": if msg.node_id in self.clients: del self.clients[msg.node_id] diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 842cb4b052..3de2b46e9f 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -868,7 +868,7 @@ def tick(self): # Fourth stage - Excess TestUser1 have been stopped but # TestUser2/TestUser3 have not reached stop timeout yet, so # their number are unchanged - self.assertEqual(master.state, STATE_SPAWNING) + self.assertEqual(master.state, STATE_RUNNING) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} @@ -950,6 +950,8 @@ def tick(self): self.assertTrue(time.time() - ts <= 5, master.state) sleep() + master.stop() + def test_distributed_shape_stop_and_restart(self): """ Test stopping and then restarting a LoadTestShape @@ -1001,6 +1003,81 @@ def tick(self): self.assertEqual(2, worker.user_count, "Shape test has not started again correctly") master.stop() + def test_distributed_shape_statuses_transition(self): + """ + Full integration test that starts both a MasterRunner and five WorkerRunner instances + The goal of this test is to validate the status on the master is correctly transitioned for each of the + test phases. + """ + + class TestUser1(User): + @task + def my_task(self): + gevent.sleep(600) + + class TestShape(LoadTestShape): + def tick(self): + run_time = self.get_run_time() + if run_time < 10: + return 5, 1 + elif run_time < 20: + return 10, 1 + elif run_time < 30: + return 15, 1 + else: + return None + + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + stop_timeout = 0 + master_env = Environment(user_classes=[TestUser1], shape_class=TestShape(), stop_timeout=stop_timeout) + master_env.shape_class.reset_time() + master = master_env.create_master_runner("*", 0) + + workers = [] + for i in range(5): + worker_env = Environment(user_classes=[TestUser1]) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) + workers.append(worker) + + # Give workers time to connect + sleep(0.1) + + self.assertEqual(STATE_INIT, master.state) + self.assertEqual(5, len(master.clients.ready)) + + statuses = [] + + ts = time.perf_counter() + + master.start_shape() + + while master.state != STATE_STOPPED: + self.assertTrue(time.perf_counter() - ts <= 40) + statuses.append((time.perf_counter() - ts, master.state, master.user_count)) + sleep(0.1) + + self.assertEqual(statuses[0][1], STATE_INIT) + + stage = 1 + for (t1, state1, user_count1), (t2, state2, user_count2) in zip(statuses[:-1], statuses[1:]): + if state1 == STATE_SPAWNING and state2 == STATE_RUNNING and stage == 1: + self.assertTrue(4 <= t2 <= 6) + elif state1 == STATE_RUNNING and state2 == STATE_SPAWNING and stage == 1: + self.assertTrue(9 <= t2 <= 11) + stage += 1 + elif state1 == STATE_SPAWNING and state2 == STATE_RUNNING and stage == 2: + self.assertTrue(14 <= t2 <= 16) + elif state1 == STATE_RUNNING and state2 == STATE_SPAWNING and stage == 2: + self.assertTrue(19 <= t2 <= 21) + stage += 1 + elif state1 == STATE_SPAWNING and state2 == STATE_RUNNING and stage == 3: + self.assertTrue(24 <= t2 <= 26) + elif state1 == STATE_RUNNING and state2 == STATE_SPAWNING and stage == 3: + self.assertTrue(29 <= t2 <= 31) + stage += 1 + elif state1 == STATE_RUNNING and state2 == STATE_STOPPED and stage == 3: + self.assertTrue(31 <= t2 <= 31) + class TestMasterRunner(LocustTestCase): def setUp(self): From 53e98610bb5bed53bd4d43358797fea8628ce845 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 24 Jun 2021 22:42:30 -0400 Subject: [PATCH 109/139] [draft] Ensure best-effort round-robin dispatch of users to workers --- locust/dispatch.py | 112 ++++++- locust/test/test_dispatch.py | 609 ++++++++++++++++++++++------------- locust/test/test_runners.py | 12 +- 3 files changed, 501 insertions(+), 232 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index cc2138d6a0..48529cea56 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -121,7 +121,7 @@ def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]: else: while not self._all_users_have_been_dispatched: ts = time.perf_counter() - yield self._users_to_dispatch_for_current_iteration + yield self._users_to_dispatch_for_current_iteration() if not self._all_users_have_been_dispatched: delta = time.perf_counter() - ts sleep_duration = max(0.0, self._wait_between_dispatch - delta) @@ -130,6 +130,15 @@ def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]: @property def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(self) -> bool: + if self._dispatched_user_count >= sum(self._user_classes_count.values()): + # There is already more users running in total than the total desired user count + return True + + if self._dispatched_user_count + self._user_count_per_dispatch > sum(self._user_classes_count.values()): + # There is already more users running in total than the total desired user count + return True + + # TODO: Still needed? # The following calculates the number of users left to dispatch # taking into account workers that have an excess of users. user_count_left_to_dispatch_excluding_excess_users = sum( @@ -140,10 +149,8 @@ def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(sel ) for user_class in self._user_classes_count.keys() ) - return self._user_count_per_dispatch >= user_count_left_to_dispatch_excluding_excess_users - @property def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: """ Compute the users to dispatch for the current dispatch iteration. @@ -189,10 +196,37 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: for j, worker_node_id in enumerate(itertools.cycle(sorted(self._users_left_to_assigned.keys()))): assert j < int( - 2 * self._number_of_workers + 10 * self._number_of_workers ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + if ( + self._dispatched_user_count == sum(self._user_classes_count.values()) + or (self._user_count_per_dispatch - user_count_in_current_dispatch) + >= self._user_count_left_to_assigned + ): + self._dispatched_users.update(self._desired_users_assigned_to_workers) + self._users_left_to_assigned.update( + { + worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} + for worker_node_id, user_classes_count in self._dispatched_users.items() + } + ) + break if self._users_left_to_assigned[worker_node_id][user_class_to_add] == 0: continue + # if self._desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration: + # self._dispatched_users.update(self._desired_users_assigned_to_workers) + # self._users_left_to_assigned.update( + # { + # worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} + # for worker_node_id, user_classes_count in self._dispatched_users.items() + # } + # ) + # user_count_in_current_dispatch = self._user_count_per_dispatch + # break + if self._try_next_worker_in_order_to_stay_balanced_during_ramp_up( + worker_node_id, user_class_to_add + ): + continue self._dispatched_users[worker_node_id][user_class_to_add] += 1 self._users_left_to_assigned[worker_node_id][user_class_to_add] -= 1 user_count_in_current_dispatch += 1 @@ -201,6 +235,9 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: if user_count_in_current_dispatch == self._user_count_per_dispatch: break + if self._dispatched_user_count == sum(self._user_classes_count.values()): + break + return { worker_node_id: dict(sorted(user_classes_count.items(), key=itemgetter(0))) for worker_node_id, user_classes_count in sorted(self._dispatched_users.items(), key=itemgetter(0)) @@ -212,8 +249,11 @@ def _number_of_workers(self) -> int: @property def _all_users_have_been_dispatched(self) -> bool: - user_count_left_to_dispatch = sum(map(sum, map(dict.values, self._users_left_to_assigned.values()))) - return user_count_left_to_dispatch == 0 + return self._user_count_left_to_assigned == 0 + + @property + def _user_count_left_to_assigned(self) -> int: + return sum(map(sum, map(dict.values, self._users_left_to_assigned.values()))) def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_class_to_add: str) -> bool: """ @@ -254,10 +294,6 @@ def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_cla return True return False - def _dispatched_user_class_count(self, user_class: str) -> int: - """Number of dispatched users at this time for the given user class""" - return sum(map(itemgetter(user_class), self._dispatched_users.values())) - def _adding_this_user_class_respects_distribution_better_than_adding_any_other_user_class( self, user_class_to_add: str ) -> bool: @@ -331,6 +367,62 @@ def _dispatched_user_classes_count(self) -> Dict[str, int]: user_class: self._dispatched_user_class_count(user_class) for user_class in self._user_classes_count.keys() } + def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( + self, worker_node_id_to_add_user_on: str, user_class: str + ) -> bool: + """ + Whether to skip to the next worker or not. This is done so that + each worker runs approximately the same amount of users during a ramp-up. + """ + if self._dispatched_user_count == 0: + return False + + workers_user_count = { + worker_node_id: sum(dispatched_users_on_worker.values()) + for worker_node_id, dispatched_users_on_worker in self._dispatched_users.items() + } + + ideal_worker_node_ids = [ + ideal_worker_node_id + for ideal_worker_node_id in workers_user_count.keys() + if workers_user_count[ideal_worker_node_id] + 1 - min(workers_user_count.values()) < 2 + ] + + if worker_node_id_to_add_user_on in ideal_worker_node_ids: + return False + + # Filter out the workers having more users than what is required at the end of the dispatch cycle + workers_user_count_without_excess_users = { + worker_node_id: user_count + for worker_node_id, user_count in workers_user_count.items() + if user_count < sum(self._desired_users_assigned_to_workers[worker_node_id].values()) + } + + if worker_node_id_to_add_user_on not in workers_user_count_without_excess_users: + return True + + if len(workers_user_count_without_excess_users) == 1: + return False + + if workers_user_count_without_excess_users[worker_node_id_to_add_user_on] + 1 - min( + workers_user_count.values() + ) >= 2 and any( + self._users_left_to_assigned[ideal_worker_node_id][user_class] > 0 + for ideal_worker_node_id in ideal_worker_node_ids + ): + return True + + return False + + def _dispatched_user_class_count(self, user_class: str) -> int: + """Number of dispatched users at this time for the given user class""" + return sum(map(itemgetter(user_class), self._dispatched_users.values())) + + @property + def _dispatched_user_count(self) -> int: + """Number of dispatched users at this time""" + return sum(map(sum, map(dict.values, self._dispatched_users.values()))) + class _WorkersUsersAssignor: """Helper to compute the users assigned to the workers""" diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 2848483e9d..13b91fa275 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -300,8 +300,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -312,9 +312,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -324,9 +324,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -336,9 +336,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, + "1": {"User1": 1, "User2": 1, "User3": 0}, "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -349,8 +349,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -361,8 +361,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -373,8 +373,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -426,8 +426,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -438,9 +438,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -450,9 +450,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -462,9 +462,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, + "1": {"User1": 1, "User2": 1, "User3": 0}, "2": {"User1": 1, "User2": 1, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -475,8 +475,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -487,8 +487,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -499,8 +499,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -540,8 +540,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -552,9 +552,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -565,8 +565,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -577,8 +577,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -618,8 +618,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 0}, - "2": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) @@ -630,9 +630,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -643,8 +643,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -655,8 +655,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -696,9 +696,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 0, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 0, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -709,8 +709,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -750,9 +750,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) delta = time.time() - ts @@ -763,8 +763,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -861,8 +861,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) @@ -873,9 +873,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -885,9 +885,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -898,8 +898,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -966,8 +966,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) @@ -978,9 +978,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -990,9 +990,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1003,8 +1003,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1059,9 +1059,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1072,8 +1072,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1128,9 +1128,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1141,8 +1141,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1185,8 +1185,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) @@ -1198,8 +1198,8 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): next(users_dispatcher), { "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 0}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1242,9 +1242,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 1, "User3": 0}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts @@ -1298,31 +1298,35 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - -class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTarget(unittest.TestCase): - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} + worker_node1.user_classes_count = {"User1": 1, "User2": 1, "User3": 0} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} + worker_node2.user_classes_count = {"User1": 1, "User2": 1, "User3": 0} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} + worker_node3.user_classes_count = {"User1": 1, "User2": 1, "User3": 0} + worker_node4 = WorkerNode("4") + worker_node4.user_classes_count = {"User1": 1, "User2": 0, "User3": 1} + worker_node5 = WorkerNode("5") + worker_node5.user_classes_count = {"User1": 0, "User2": 0, "User3": 2} users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.5, + worker_nodes=[worker_node1, worker_node2, worker_node3, worker_node4, worker_node5], + user_classes_count={"User1": 5, "User2": 5, "User3": 5}, + spawn_rate=3, ) - sleep_time = 1 / 0.5 + sleep_time = 1 ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 0}, + "3": {"User1": 1, "User2": 1, "User3": 0}, + "4": {"User1": 1, "User2": 1, "User3": 1}, + "5": {"User1": 1, "User2": 0, "User3": 2}, }, ) delta = time.time() - ts @@ -1332,25 +1336,68 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + "4": {"User1": 1, "User2": 1, "User3": 1}, + "5": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + +class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTargetAndMoreTotalUsers(unittest.TestCase): + # TODO: Docstring + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 7} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.5, + ) + ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 1}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 7} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=1, + ) ts = time.time() self.assertDictEqual( @@ -1362,14 +1409,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") @@ -1380,46 +1427,101 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=1, + spawn_rate=2, ) - sleep_time = 1 - ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 0}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 7} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2.4, + ) + ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 7} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=3, + ) ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 1}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 7} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=4, + ) ts = time.time() self.assertDictEqual( @@ -1431,14 +1533,14 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") @@ -1449,34 +1551,56 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=2, + spawn_rate=9, ) - sleep_time = 1 - ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + +class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTargetAndLessTotalUsers(unittest.TestCase): + # TODO: Docstring + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 4} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 4} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.5, + ) + + sleep_time = 1 / 0.5 + ts = time.time() self.assertDictEqual( next(users_dispatcher), { "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 1}, + "2": {"User1": 4, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 4, "User3": 0}, }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1495,29 +1619,29 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} + worker_node2.user_classes_count = {"User1": 4} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} + worker_node3.user_classes_count = {"User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=2.4, + spawn_rate=1, ) - sleep_time = 2 / 2.4 + sleep_time = 1 ts = time.time() self.assertDictEqual( next(users_dispatcher), { "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 0}, + "2": {"User1": 4, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 4, "User3": 0}, }, ) delta = time.time() - ts @@ -1527,14 +1651,33 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 0, "User2": 0, "User3": 1}, - "2": {"User1": 5, "User2": 0, "User3": 1}, - "3": {"User1": 0, "User2": 7, "User3": 1}, + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) delta = time.time() - ts self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 4} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 4} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2, + ) + ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1545,7 +1688,38 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(0 <= delta <= 0.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 4} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 4} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=2.4, + ) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) @@ -1556,9 +1730,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} + worker_node2.user_classes_count = {"User1": 4} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} + worker_node3.user_classes_count = {"User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -1587,9 +1761,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} + worker_node2.user_classes_count = {"User1": 4} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} + worker_node3.user_classes_count = {"User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -1618,9 +1792,9 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} + worker_node2.user_classes_count = {"User1": 4} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} + worker_node3.user_classes_count = {"User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], @@ -1742,10 +1916,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 2, "User2": 3}, - "2": {"User1": 0, "User2": 0}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 1, "User2": 1}, + "2": {"User1": 0, "User2": 1}, + "3": {"User1": 0, "User2": 1}, + "4": {"User1": 1, "User2": 0}, }, ) delta = time.time() - ts @@ -1758,10 +1932,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 4, "User2": 6}, - "2": {"User1": 0, "User2": 0}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 1, "User2": 2}, + "2": {"User1": 1, "User2": 2}, + "3": {"User1": 1, "User2": 1}, + "4": {"User1": 1, "User2": 1}, }, ) delta = time.time() - ts @@ -1774,10 +1948,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 5, "User2": 10}, - "2": {"User1": 0, "User2": 0}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 2, "User2": 2}, + "2": {"User1": 1, "User2": 3}, + "3": {"User1": 1, "User2": 3}, + "4": {"User1": 1, "User2": 2}, }, ) delta = time.time() - ts @@ -1790,10 +1964,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 0, "User2": 1}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 2, "User2": 3}, + "2": {"User1": 1, "User2": 4}, + "3": {"User1": 2, "User2": 3}, + "4": {"User1": 2, "User2": 3}, }, ) delta = time.time() - ts @@ -1806,10 +1980,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, + "1": {"User1": 3, "User2": 4}, "2": {"User1": 2, "User2": 4}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "3": {"User1": 2, "User2": 4}, + "4": {"User1": 2, "User2": 4}, }, ) delta = time.time() - ts @@ -1822,10 +1996,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 3, "User2": 8}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 3, "User2": 5}, + "2": {"User1": 2, "User2": 6}, + "3": {"User1": 2, "User2": 5}, + "4": {"User1": 3, "User2": 4}, }, ) delta = time.time() - ts @@ -1838,10 +2012,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 5, "User2": 11}, - "3": {"User1": 0, "User2": 0}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 3, "User2": 6}, + "2": {"User1": 3, "User2": 6}, + "3": {"User1": 3, "User2": 6}, + "4": {"User1": 3, "User2": 5}, }, ) delta = time.time() - ts @@ -1854,10 +2028,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, - "3": {"User1": 1, "User2": 1}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 4, "User2": 6}, + "2": {"User1": 3, "User2": 7}, + "3": {"User1": 3, "User2": 7}, + "4": {"User1": 4, "User2": 6}, }, ) delta = time.time() - ts @@ -1870,10 +2044,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, - "3": {"User1": 2, "User2": 5}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 4, "User2": 8}, + "2": {"User1": 3, "User2": 8}, + "3": {"User1": 4, "User2": 7}, + "4": {"User1": 4, "User2": 7}, }, ) delta = time.time() - ts @@ -1886,10 +2060,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, + "1": {"User1": 5, "User2": 8}, + "2": {"User1": 4, "User2": 9}, "3": {"User1": 4, "User2": 8}, - "4": {"User1": 0, "User2": 0}, + "4": {"User1": 4, "User2": 8}, }, ) delta = time.time() - ts @@ -1902,10 +2076,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, - "3": {"User1": 6, "User2": 11}, - "4": {"User1": 0, "User2": 0}, + "1": {"User1": 5, "User2": 9}, + "2": {"User1": 4, "User2": 10}, + "3": {"User1": 5, "User2": 9}, + "4": {"User1": 5, "User2": 8}, }, ) delta = time.time() - ts @@ -1918,10 +2092,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, - "3": {"User1": 6, "User2": 13}, - "4": {"User1": 1, "User2": 2}, + "1": {"User1": 5, "User2": 10}, + "2": {"User1": 5, "User2": 10}, + "3": {"User1": 5, "User2": 10}, + "4": {"User1": 5, "User2": 10}, }, ) delta = time.time() - ts @@ -1934,10 +2108,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, - "3": {"User1": 6, "User2": 13}, - "4": {"User1": 3, "User2": 5}, + "1": {"User1": 6, "User2": 11}, + "2": {"User1": 5, "User2": 11}, + "3": {"User1": 5, "User2": 11}, + "4": {"User1": 6, "User2": 10}, }, ) delta = time.time() - ts @@ -1950,10 +2124,10 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): self.assertDictEqual( dispatched_users, { - "1": {"User1": 7, "User2": 12}, - "2": {"User1": 6, "User2": 13}, - "3": {"User1": 6, "User2": 13}, - "4": {"User1": 5, "User2": 8}, + "1": {"User1": 6, "User2": 12}, + "2": {"User1": 6, "User2": 12}, + "3": {"User1": 6, "User2": 11}, + "4": {"User1": 6, "User2": 11}, }, ) delta = time.time() - ts @@ -1992,7 +2166,10 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw in one second due to bugs in the dispatcher. This test ensures that this problem can't reappear in the future. """ - worker_nodes = [WorkerNode(str(i)) for i in range(1, 21)] + # Prepend "0" to worker name under 10, because workers are sorted in alphabetical orders. + # It's simply makes the test easier to read and debug, but in reality, the worker "10" would come + # before worker "1". + worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 21)] for worker_node in worker_nodes: worker_node.user_classes_count = {"User1": 0} diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 3de2b46e9f..4901c3dc37 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -785,7 +785,7 @@ def tick(self): # First stage ts = time.time() while master.state != STATE_SPAWNING: - self.assertTrue(time.time() - ts <= 1) + self.assertTrue(time.time() - ts <= 1, master.state) sleep() sleep(5 - (time.time() - ts)) # runtime = 5s self.assertEqual(STATE_RUNNING, master.state) @@ -809,7 +809,7 @@ def tick(self): # Second stage ts = time.time() while master.state != STATE_SPAWNING: - self.assertTrue(time.time() - ts <= 1) + self.assertTrue(time.time() - ts <= 1, master.state) sleep() sleep(5 - (time.time() - ts)) # runtime = 15s self.assertEqual(STATE_RUNNING, master.state) @@ -833,12 +833,12 @@ def tick(self): # Third stage ts = time.time() while master.state != STATE_SPAWNING: - self.assertTrue(time.time() - ts <= 1) + self.assertTrue(time.time() - ts <= 1, master.state) sleep() sleep(10 - (time.time() - ts)) # runtime = 30s ts = time.time() while master.state != STATE_RUNNING: - self.assertTrue(time.time() - ts <= 1) + self.assertTrue(time.time() - ts <= 1, master.state) sleep() self.assertEqual(STATE_RUNNING, master.state) w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} @@ -861,7 +861,7 @@ def tick(self): # Fourth stage ts = time.time() while master.state != STATE_SPAWNING: - self.assertTrue(time.time() - ts <= 1) + self.assertTrue(time.time() - ts <= 1, master.state) sleep() sleep(5 - (time.time() - ts)) # runtime = 45s @@ -889,7 +889,7 @@ def tick(self): # Fourth stage - All users are now at the desired number ts = time.time() while master.state != STATE_RUNNING: - self.assertTrue(time.time() - ts <= 1) + self.assertTrue(time.time() - ts <= 1, master.state) sleep() delta = time.time() - ts w1 = {"TestUser1": 1, "TestUser2": 0, "TestUser3": 0} From 89d8b74f443e691f8619fd759db17ec26344393d Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 11:20:38 -0400 Subject: [PATCH 110/139] Prepend `LOCUST_` to environment variables --- locust/runners.py | 2 +- locust/test/test_runners.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 6998b9694e..b078a51f75 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -1046,7 +1046,7 @@ def worker(self): self.client.send(Message("client_stopped", None, self.client_id)) # +additional_wait is just a small buffer to account for the random network latencies and/or other # random delays inherent to distributed systems. - additional_wait = int(os.getenv("WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", 0)) + additional_wait = int(os.getenv("LOCUST_WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", 0)) gevent.sleep((self.environment.stop_timeout or 0) + additional_wait) self.client.send(Message("client_ready", None, self.client_id)) self.worker_state = STATE_INIT diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 4901c3dc37..576fb0241a 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -751,9 +751,10 @@ def tick(self): else: return None - worker_additional_wait_before_ready_after_stop = 5 + locust_worker_additional_wait_before_ready_after_stop = 5 with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3), _patch_env( - "WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", str(worker_additional_wait_before_ready_after_stop) + "LOCUST_WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", + str(locust_worker_additional_wait_before_ready_after_stop), ): stop_timeout = 5 master_env = Environment( @@ -922,7 +923,7 @@ def tick(self): ts = time.time() while len(master.clients.ready) != len(workers): self.assertTrue( - time.time() - ts <= stop_timeout + worker_additional_wait_before_ready_after_stop, + time.time() - ts <= stop_timeout + locust_worker_additional_wait_before_ready_after_stop, f"expected {len(workers)} workers to be ready but only {len(master.clients.ready)} workers are", ) sleep() From eb3c9acf38a79101b88312fcf7fdb3099a6d1c36 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 11:21:04 -0400 Subject: [PATCH 111/139] Remove commented code which is no longer in use --- locust/dispatch.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 48529cea56..cb60549743 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -213,16 +213,6 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: break if self._users_left_to_assigned[worker_node_id][user_class_to_add] == 0: continue - # if self._desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration: - # self._dispatched_users.update(self._desired_users_assigned_to_workers) - # self._users_left_to_assigned.update( - # { - # worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} - # for worker_node_id, user_classes_count in self._dispatched_users.items() - # } - # ) - # user_count_in_current_dispatch = self._user_count_per_dispatch - # break if self._try_next_worker_in_order_to_stay_balanced_during_ramp_up( worker_node_id, user_class_to_add ): From 49fbeb133053373c43693c76ec84e703e520d807 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:03:56 -0400 Subject: [PATCH 112/139] Implement `LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP` This allows to configure the duration to wait for the reported user count to match the desired value before considering a spawning complete. --- locust/runners.py | 35 ++++++++++++++++++++++++++++++++--- locust/test/test_runners.py | 26 ++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index b078a51f75..3e023cebf4 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- +import functools import json import logging import os +import re import socket import sys import time @@ -697,9 +699,9 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: self.quit() # Wait a little for workers to report their users to the master - # so that we can give an accurate log message below, i.e. "All users spawned [...]". - # Otherwise, the logged user count might be less than the target user count. - timeout = gevent.Timeout(0.1) + # so that we can give an accurate log message below and fire the `spawning_complete` event + # when the user count is really at the desired value. + timeout = gevent.Timeout(self._wait_for_workers_report_after_ramp_up()) timeout.start() try: while self.user_count != self.target_user_count: @@ -719,6 +721,33 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: ) ) + @functools.lru_cache() + def _wait_for_workers_report_after_ramp_up(self) -> float: + """ + The amount of time to wait after a ramp-up in order for all the workers to report their state + to the master. If not supplied by the user, it is 100ms by default. If the supplied value is a number, + it is taken as-is. If the supplied value is a pattern like "some_number * WORKER_REPORT_INTERVAL", + the value will be "some_number * WORKER_REPORT_INTERVAL". The most sensible value would be something + like "1.25 * WORKER_REPORT_INTERVAL". However, some users might find it too high, so it is left + to a really small value of 100ms by default. + """ + locust_wait_for_workers_report_after_ramp_up = os.getenv("LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP") + if locust_wait_for_workers_report_after_ramp_up is None: + return 0.1 + + match = re.search( + r"^(?P(\d+)|(\d+\.\d+))[ ]*\*[ ]*WORKER_REPORT_INTERVAL$", + locust_wait_for_workers_report_after_ramp_up, + ) + if match is None: + assert float(locust_wait_for_workers_report_after_ramp_up) >= 0 + return float(locust_wait_for_workers_report_after_ramp_up) + + if match is not None: + return float(match.group("coeff")) * WORKER_REPORT_INTERVAL + + assert False, "not supposed to reach that" + def stop(self): if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]: logger.debug("Stopping...") diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 576fb0241a..81d43e19ec 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1865,6 +1865,32 @@ def on_custom_msg(msg, **kw): msg = self.mocked_log.warning[0] self.assertIn("Unknown message type recieved from worker", msg) + def test_wait_for_workers_report_after_ramp_up(self): + def assert_cache_hits(): + self.assertEqual(master._wait_for_workers_report_after_ramp_up.cache_info().hits, 0) + master._wait_for_workers_report_after_ramp_up() + self.assertEqual(master._wait_for_workers_report_after_ramp_up.cache_info().hits, 1) + + master = self.get_runner() + + master._wait_for_workers_report_after_ramp_up.cache_clear() + self.assertEqual(master._wait_for_workers_report_after_ramp_up(), 0.1) + assert_cache_hits() + + master._wait_for_workers_report_after_ramp_up.cache_clear() + with _patch_env("LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP", "5.7"): + self.assertEqual(master._wait_for_workers_report_after_ramp_up(), 5.7) + assert_cache_hits() + + master._wait_for_workers_report_after_ramp_up.cache_clear() + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=1.5), _patch_env( + "LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP", "5.7 * WORKER_REPORT_INTERVAL" + ): + self.assertEqual(master._wait_for_workers_report_after_ramp_up(), 5.7 * 1.5) + assert_cache_hits() + + master._wait_for_workers_report_after_ramp_up.cache_clear() + @contextmanager def _patch_env(name: str, value: str): From e9c577e0d31191498255d387ff1374d048ca11b6 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:04:39 -0400 Subject: [PATCH 113/139] Use a named variable instead of hard-coding a constant in test --- locust/test/test_runners.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 81d43e19ec..a8f410a8b3 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1060,24 +1060,25 @@ def tick(self): self.assertEqual(statuses[0][1], STATE_INIT) stage = 1 + tolerance = 1 # in s for (t1, state1, user_count1), (t2, state2, user_count2) in zip(statuses[:-1], statuses[1:]): if state1 == STATE_SPAWNING and state2 == STATE_RUNNING and stage == 1: - self.assertTrue(4 <= t2 <= 6) + self.assertTrue(5 - tolerance <= t2 <= 5 + tolerance) elif state1 == STATE_RUNNING and state2 == STATE_SPAWNING and stage == 1: - self.assertTrue(9 <= t2 <= 11) + self.assertTrue(10 - tolerance <= t2 <= 10 + tolerance) stage += 1 elif state1 == STATE_SPAWNING and state2 == STATE_RUNNING and stage == 2: - self.assertTrue(14 <= t2 <= 16) + self.assertTrue(15 - tolerance <= t2 <= 15 + tolerance) elif state1 == STATE_RUNNING and state2 == STATE_SPAWNING and stage == 2: - self.assertTrue(19 <= t2 <= 21) + self.assertTrue(20 - tolerance <= t2 <= 20 + tolerance) stage += 1 elif state1 == STATE_SPAWNING and state2 == STATE_RUNNING and stage == 3: - self.assertTrue(24 <= t2 <= 26) + self.assertTrue(25 - tolerance <= t2 <= 25 + tolerance) elif state1 == STATE_RUNNING and state2 == STATE_SPAWNING and stage == 3: - self.assertTrue(29 <= t2 <= 31) + self.assertTrue(30 - tolerance <= t2 <= 30 + tolerance) stage += 1 elif state1 == STATE_RUNNING and state2 == STATE_STOPPED and stage == 3: - self.assertTrue(31 <= t2 <= 31) + self.assertTrue(30 - tolerance <= t2 <= 30 + tolerance) class TestMasterRunner(LocustTestCase): From 7aa119360821161f2734cbd7b59b2a3ab034e6a7 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:05:14 -0400 Subject: [PATCH 114/139] Fix `_patch_env` helper not correctly tearing down itself --- locust/test/test_runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index a8f410a8b3..d4b75178b8 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1901,7 +1901,7 @@ def _patch_env(name: str, value: str): yield finally: if prev_value is None: - os.unsetenv(name) + del os.environ[name] else: os.environ[name] = prev_value From 1efb253ba7d37e116410b4f7da33bfdb52851f71 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:08:30 -0400 Subject: [PATCH 115/139] Clear all `functools.lru_cache` between tests This ensures that no state is persisted from one test to another --- locust/test/testcases.py | 4 +++- locust/test/util.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/locust/test/testcases.py b/locust/test/testcases.py index 1ec9f00d3a..5d54af9b4e 100644 --- a/locust/test/testcases.py +++ b/locust/test/testcases.py @@ -15,7 +15,7 @@ from locust.event import Events from locust.env import Environment from locust.test.mock_logging import MockedLoggingHandler - +from locust.test.util import clear_all_functools_lru_cache app = Flask(__name__) app.jinja_options["extensions"].append("jinja2.ext.do") @@ -183,6 +183,8 @@ def tearDown(self): [logging.root.addHandler(h) for h in self._root_log_handlers] self.mocked_log.reset() + clear_all_functools_lru_cache() + class WebserverTestCase(LocustTestCase): """ diff --git a/locust/test/util.py b/locust/test/util.py index b1ea5fd34d..a66edf62e9 100644 --- a/locust/test/util.py +++ b/locust/test/util.py @@ -1,3 +1,5 @@ +import functools +import gc import os import socket @@ -59,3 +61,13 @@ def create_tls_cert(hostname): ) return cert_pem, key_pem + + +def clear_all_functools_lru_cache() -> None: + # Clear all `functools.lru_cache` to ensure that no state are persisted from one test to another. + # Taken from https://stackoverflow.com/a/50699209. + gc.collect() + wrappers = [a for a in gc.get_objects() if isinstance(a, functools._lru_cache_wrapper)] + assert len(wrappers) > 0 + for wrapper in wrappers: + wrapper.cache_clear() From 9651768d8921fc3bc5499426875fd0d4315b371c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:11:33 -0400 Subject: [PATCH 116/139] Improve dispatch for some corner cases + code cleanup --- locust/dispatch.py | 201 ++++++++------- locust/test/test_dispatch.py | 464 +++++++++++++---------------------- 2 files changed, 286 insertions(+), 379 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index cb60549743..b3722b02c2 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -63,6 +63,17 @@ def __init__(self, worker_nodes: "List[WorkerNode]", user_classes_count: Dict[st self._user_classes = sorted(user_classes_count.keys()) + # Represents the already running users among the workers at the start of this dispatch cycle + self._initial_dispatched_users = { + worker_node.id: { + user_class: worker_node.user_classes_count.get(user_class, 0) + for user_class in self._user_classes_count.keys() + } + for worker_node in self._worker_nodes + } + + self._desired_user_count = sum(self._user_classes_count.values()) + self._spawn_rate = spawn_rate self._desired_users_assigned_to_workers = _WorkersUsersAssignor( @@ -79,7 +90,7 @@ def __init__(self, worker_nodes: "List[WorkerNode]", user_classes_count: Dict[st user_class: max( 0, self._desired_users_assigned_to_workers[worker_node.id][user_class] - - self._initial_dispatched_users()[worker_node.id][user_class], + - self._initial_dispatched_users[worker_node.id][user_class], ) for user_class in self._user_classes_count.keys() } @@ -94,22 +105,11 @@ def __init__(self, worker_nodes: "List[WorkerNode]", user_classes_count: Dict[st # to keep track of the number of dispatched users for the current dispatch cycle. # It is essentially the same thing as for the `effective_assigned_users` dictionary, # but in reverse. - self._dispatched_users = deepcopy(self._initial_dispatched_users()) + self._dispatched_users = deepcopy(self._initial_dispatched_users) # Initialize the generator that is used in `__next__` self._dispatcher_generator = self._dispatcher() - @functools.lru_cache() - def _initial_dispatched_users(self) -> Dict[str, Dict[str, int]]: - """Represents the already running users among the workers at the start of the dispatch""" - return { - worker_node.id: { - user_class: worker_node.user_classes_count.get(user_class, 0) - for user_class in self._user_classes_count.keys() - } - for worker_node in self._worker_nodes - } - def __next__(self) -> Dict[str, Dict[str, int]]: return next(self._dispatcher_generator) @@ -130,24 +130,48 @@ def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]: @property def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(self) -> bool: - if self._dispatched_user_count >= sum(self._user_classes_count.values()): + if self._dispatched_user_count >= self._desired_user_count: # There is already more users running in total than the total desired user count return True - if self._dispatched_user_count + self._user_count_per_dispatch > sum(self._user_classes_count.values()): - # There is already more users running in total than the total desired user count + if self._dispatched_user_count + self._user_count_per_dispatch > self._desired_user_count: + # The spawn rate greater than the remaining users to dispatch return True - # TODO: Still needed? - # The following calculates the number of users left to dispatch - # taking into account workers that have an excess of users. - user_count_left_to_dispatch_excluding_excess_users = sum( - max( + user_classes_count_left_to_dispatch_excluding_excess_users = { + user_class: max( 0, sum(map(itemgetter(user_class), self._desired_users_assigned_to_workers.values())) - self._dispatched_user_class_count(user_class), ) for user_class in self._user_classes_count.keys() + } + + workers_user_count = self._workers_user_count + if ( + sum( + 1 + for user_class, user_class_count_left_to_dispatch in user_classes_count_left_to_dispatch_excluding_excess_users.items() + if user_class_count_left_to_dispatch != 0 + for worker_node in self._worker_nodes + if ( + workers_user_count[worker_node.id] + < sum(self._desired_users_assigned_to_workers[worker_node.id].values()) + ) + if ( + ( + self._desired_users_assigned_to_workers[worker_node.id][user_class] + - self._dispatched_users[worker_node.id][user_class] + ) + > 0 + ) + ) + == 0 + ): + return True + + user_count_left_to_dispatch_excluding_excess_users = sum( + user_classes_count_left_to_dispatch_excluding_excess_users.values() ) return self._user_count_per_dispatch >= user_count_left_to_dispatch_excluding_excess_users @@ -155,78 +179,62 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: """ Compute the users to dispatch for the current dispatch iteration. """ - if all( - self._dispatched_user_class_count(user_class) >= user_count - for user_class, user_count in self._user_classes_count.items() - ): - # User count for every user class is greater than or equal to the target user count of each class. - # This means that we're at the last iteration of this dispatch cycle. If some user classes are in - # excess, this last iteration will stop those excess users. - self._dispatched_users.update(self._desired_users_assigned_to_workers) - self._users_left_to_assigned.update( - { - worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} - for worker_node_id, user_classes_count in self._dispatched_users.items() - } - ) + user_count_in_current_dispatch = 0 - else: - user_count_in_current_dispatch = 0 + for i, user_class_to_add in enumerate(itertools.cycle(self._user_classes)): + # For large number of user classes and large number of workers, this assertion might fail. + # If this happens, you can remove it or increase the threshold. Right now, the assertion + # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). + assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) - for i, user_class_to_add in enumerate(itertools.cycle(self._user_classes)): - # For large number of user classes and large number of workers, this assertion might fail. - # If this happens, you can remove it or increase the threshold. Right now, the assertion - # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). - assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) + if self._all_users_have_been_dispatched: + break - if self._all_users_have_been_dispatched: - break + if all( + self._dispatched_user_class_count(user_class) >= user_count + for user_class, user_count in self._user_classes_count.items() + ): + break - if all( - self._dispatched_user_class_count(user_class) >= user_count - for user_class, user_count in self._user_classes_count.items() + if self._dispatched_user_class_count(user_class_to_add) >= self._user_classes_count[user_class_to_add]: + continue + + if self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up(user_class_to_add): + continue + + for j, worker_node_id in enumerate(itertools.cycle(sorted(self._users_left_to_assigned.keys()))): + assert j < int( + 2 * self._number_of_workers + ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + if ( + self._dispatched_user_count == self._desired_user_count + or (self._user_count_per_dispatch - user_count_in_current_dispatch) + >= self._user_count_left_to_assigned ): + # This means that we're at the last iteration of this dispatch cycle. If some user + # classes are in excess, this last iteration will stop those excess users. + self._dispatched_users.update(self._desired_users_assigned_to_workers) + self._users_left_to_assigned.update( + { + worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} + for worker_node_id, user_classes_count in self._dispatched_users.items() + } + ) break - - if self._dispatched_user_class_count(user_class_to_add) >= self._user_classes_count[user_class_to_add]: + if self._users_left_to_assigned[worker_node_id][user_class_to_add] == 0: continue - - if self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up(user_class_to_add): + if self._try_next_worker_in_order_to_stay_balanced_during_ramp_up(worker_node_id, user_class_to_add): continue + self._dispatched_users[worker_node_id][user_class_to_add] += 1 + self._users_left_to_assigned[worker_node_id][user_class_to_add] -= 1 + user_count_in_current_dispatch += 1 + break - for j, worker_node_id in enumerate(itertools.cycle(sorted(self._users_left_to_assigned.keys()))): - assert j < int( - 10 * self._number_of_workers - ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if ( - self._dispatched_user_count == sum(self._user_classes_count.values()) - or (self._user_count_per_dispatch - user_count_in_current_dispatch) - >= self._user_count_left_to_assigned - ): - self._dispatched_users.update(self._desired_users_assigned_to_workers) - self._users_left_to_assigned.update( - { - worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} - for worker_node_id, user_classes_count in self._dispatched_users.items() - } - ) - break - if self._users_left_to_assigned[worker_node_id][user_class_to_add] == 0: - continue - if self._try_next_worker_in_order_to_stay_balanced_during_ramp_up( - worker_node_id, user_class_to_add - ): - continue - self._dispatched_users[worker_node_id][user_class_to_add] += 1 - self._users_left_to_assigned[worker_node_id][user_class_to_add] -= 1 - user_count_in_current_dispatch += 1 - break - - if user_count_in_current_dispatch == self._user_count_per_dispatch: - break + if user_count_in_current_dispatch == self._user_count_per_dispatch: + break - if self._dispatched_user_count == sum(self._user_classes_count.values()): - break + if self._dispatched_user_count == self._desired_user_count: + break return { worker_node_id: dict(sorted(user_classes_count.items(), key=itemgetter(0))) @@ -345,10 +353,7 @@ def _distance_from_ideal_distribution_after_adding_this_user_class(self, user_cl @functools.lru_cache() def _desired_relative_weights(self) -> List[float]: """The relative weight of each user class we desire""" - return [ - self._user_classes_count[user_class] / sum(self._user_classes_count.values()) - for user_class in self._user_classes - ] + return [self._user_classes_count[user_class] / self._desired_user_count for user_class in self._user_classes] @functools.lru_cache() def _dispatched_user_classes_count(self) -> Dict[str, int]: @@ -367,11 +372,10 @@ def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( if self._dispatched_user_count == 0: return False - workers_user_count = { - worker_node_id: sum(dispatched_users_on_worker.values()) - for worker_node_id, dispatched_users_on_worker in self._dispatched_users.items() - } + workers_user_count = self._workers_user_count + # Represents the ideal workers on which we'd want to add the user class + # because these workers contain less users than all the other workers ideal_worker_node_ids = [ ideal_worker_node_id for ideal_worker_node_id in workers_user_count.keys() @@ -381,7 +385,8 @@ def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( if worker_node_id_to_add_user_on in ideal_worker_node_ids: return False - # Filter out the workers having more users than what is required at the end of the dispatch cycle + # Only keep the workers having less users than the target value as + # we can't add users to those workers anyway. workers_user_count_without_excess_users = { worker_node_id: user_count for worker_node_id, user_count in workers_user_count.items() @@ -400,10 +405,22 @@ def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( self._users_left_to_assigned[ideal_worker_node_id][user_class] > 0 for ideal_worker_node_id in ideal_worker_node_ids ): + # Adding the user to the current worker will result in this worker having more than 1 + # extra users compared to the other workers (condition on the left of the `and` above). + # Moreover, we know there exists at least one other worker that would better host + # the new user (condition on the right of the `and` above). Thus, we skip to the next worker node. return True return False + @property + def _workers_user_count(self) -> Dict[str, int]: + """User count currently running on each of the workers""" + return { + worker_node_id: sum(dispatched_users_on_worker.values()) + for worker_node_id, dispatched_users_on_worker in self._dispatched_users.items() + } + def _dispatched_user_class_count(self, user_class: str) -> int: """Number of dispatched users at this time for the given user class""" return sum(map(itemgetter(user_class), self._dispatched_users.values())) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 13b91fa275..98b3c27fb9 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -5,6 +5,7 @@ from locust.dispatch import UsersDispatcher from locust.runners import WorkerNode +from locust.test.util import clear_all_functools_lru_cache class TestAssignUsersToWorkers(unittest.TestCase): @@ -1353,228 +1354,58 @@ def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTargetAndMoreTotalUsers(unittest.TestCase): - # TODO: Docstring - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=0.5, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=1, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=2, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=2.4, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=3, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=4, - ) - - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} + """ + Test scenario with: + - Already more total users running than desired + - Some workers already have more users than desired + - Some workers have less users than desired + """ - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=9, - ) + def test_dispatch_users_to_3_workers(self): + for worker_node1_user_classes_count in [{}, {"User3": 1}]: + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = worker_node1_user_classes_count + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {"User1": 5} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 7} + + for spawn_rate in [0.5, 1, 2, 2.4, 3, 4, 9]: + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=spawn_rate, + ) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + clear_all_functools_lru_cache() class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTargetAndLessTotalUsers(unittest.TestCase): - # TODO: Docstring - def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): + """ + Test scenario with: + - Some users are already but there are less total users running than desired + - Some workers already have more users than desired + - Some workers have less users than desired + """ + + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_one_worker_empty(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") @@ -1619,7 +1450,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(self): worker_node1 = WorkerNode("1") worker_node1.user_classes_count = {} worker_node2 = WorkerNode("2") @@ -1664,18 +1495,18 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_empty(self): worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} + worker_node1.user_classes_count = {"User3": 1} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 4} + worker_node2.user_classes_count = {"User1": 3} worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=2, + spawn_rate=0.5, ) ts = time.time() @@ -1695,18 +1526,18 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_empty(self): worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} + worker_node1.user_classes_count = {"User3": 1} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 4} + worker_node2.user_classes_count = {"User1": 3} worker_node3 = WorkerNode("3") worker_node3.user_classes_count = {"User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=2.4, + spawn_rate=1, ) ts = time.time() @@ -1726,51 +1557,36 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_empty_and_one_user_class_requiring_no_change( + self, + ): worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} + worker_node1.user_classes_count = {"User1": 1, "User3": 1} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 4} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 4} + worker_node3.user_classes_count = {"User1": 1, "User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=3, + spawn_rate=0.5, ) + sleep_time = 1 / 0.5 + ts = time.time() self.assertDictEqual( next(users_dispatcher), { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 1, "User2": 4, "User3": 0}, }, ) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) - - def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 4} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 4} - - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=4, - ) - ts = time.time() self.assertDictEqual( next(users_dispatcher), @@ -1781,26 +1597,42 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) - def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): + def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_empty_and_one_user_class_requiring_no_change( + self, + ): worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {} + worker_node1.user_classes_count = {"User1": 1, "User3": 1} worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 4} + worker_node2.user_classes_count = {"User1": 1} worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 4} + worker_node3.user_classes_count = {"User1": 1, "User2": 4} users_dispatcher = UsersDispatcher( worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=9, + spawn_rate=1, + ) + + sleep_time = 1 + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 0, "User3": 1}, + "2": {"User1": 1, "User2": 0, "User3": 1}, + "3": {"User1": 1, "User2": 4, "User3": 0}, + }, ) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) ts = time.time() self.assertDictEqual( @@ -1812,46 +1644,102 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + def test_dispatch_users_to_3_workers_with_spawn_rate_greater_than_or_equal_to_2(self): + worker_node1_user_classes_count_cases = [{}, {"User3": 1}, {"User3": 2}] + worker_node2_user_classes_count_cases = [{"User1": 4}, {"User1": 3}, {"User1": 3}] + worker_node3_user_classes_count_cases = [{"User2": 4}, {"User2": 3}, {"User2": 3}] + for (worker_node1_user_classes_count, worker_node2_user_classes_count, worker_node3_user_classes_count) in zip( + worker_node1_user_classes_count_cases, + worker_node2_user_classes_count_cases, + worker_node3_user_classes_count_cases, + ): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = worker_node1_user_classes_count + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = worker_node2_user_classes_count + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = worker_node3_user_classes_count + + for spawn_rate in [2, 2.4, 3, 4, 9]: + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=spawn_rate, + ) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) + + clear_all_functools_lru_cache() + class TestDispatchUsersToWorkersHavingMoreUsersThanTheTarget(unittest.TestCase): def test_dispatch_users_to_3_workers(self): - worker_node1 = WorkerNode("1") - worker_node1.user_classes_count = {"User3": 15} - worker_node2 = WorkerNode("2") - worker_node2.user_classes_count = {"User1": 5} - worker_node3 = WorkerNode("3") - worker_node3.user_classes_count = {"User2": 7} - - for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: - users_dispatcher = UsersDispatcher( - worker_nodes=[worker_node1, worker_node2, worker_node3], - user_classes_count={"User1": 3, "User2": 3, "User3": 3}, - spawn_rate=spawn_rate, - ) + worker_node1_user_classes_count_cases = [{"User3": 15}, {"User3": 3}] + worker_node2_user_classes_count_cases = [{"User1": 5}, {"User1": 3}] + worker_node3_user_classes_count_cases = [{"User2": 7}, {"User2": 3}] + for ( + worker_node1_user_classes_count, + worker_node2_user_classes_count, + worker_node3_user_classes_count, + ) in itertools.product( + worker_node1_user_classes_count_cases, + worker_node2_user_classes_count_cases, + worker_node3_user_classes_count_cases, + ): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = worker_node1_user_classes_count + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = worker_node2_user_classes_count + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = worker_node3_user_classes_count + + for spawn_rate in [0.15, 0.5, 1, 2, 2.4, 3, 4, 9]: + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=spawn_rate, + ) + + ts = time.time() + self.assertDictEqual( + next(users_dispatcher), + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) - ts = time.time() - self.assertDictEqual( - next(users_dispatcher), - { - "1": {"User1": 1, "User2": 1, "User3": 1}, - "2": {"User1": 1, "User2": 1, "User3": 1}, - "3": {"User1": 1, "User2": 1, "User3": 1}, - }, - ) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + ts = time.time() + self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + delta = time.time() - ts + self.assertTrue(0 <= delta <= 0.02, delta) - ts = time.time() - self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + clear_all_functools_lru_cache() class TestDispatchUsersToWorkersHavingTheSameUsersAsTheTarget(unittest.TestCase): @@ -1887,6 +1775,8 @@ def test_dispatch_users_to_3_workers(self): delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + clear_all_functools_lru_cache() + class TestDistributionIsKeptDuringDispatch(unittest.TestCase): def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): From 110669e0f5a66ecc0560b983df89d0d28efafa75 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:13:04 -0400 Subject: [PATCH 117/139] Reduce test flakyness by waiting for desired result --- locust/test/test_runners.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index d4b75178b8..6a85f09668 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -869,7 +869,11 @@ def tick(self): # Fourth stage - Excess TestUser1 have been stopped but # TestUser2/TestUser3 have not reached stop timeout yet, so # their number are unchanged - self.assertEqual(master.state, STATE_RUNNING) + ts = time.time() + while master.state != STATE_RUNNING: + self.assertTrue(time.time() - ts <= 1, master.state) + sleep() + delta = time.time() - ts w1 = {"TestUser1": 1, "TestUser2": 1, "TestUser3": 1} w2 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} w3 = {"TestUser1": 0, "TestUser2": 1, "TestUser3": 1} @@ -885,7 +889,7 @@ def tick(self): self.assertDictEqual(w3, master.clients[workers[2].client_id].user_classes_count) self.assertDictEqual(w4, master.clients[workers[3].client_id].user_classes_count) self.assertDictEqual(w5, master.clients[workers[4].client_id].user_classes_count) - sleep(1) # runtime = 46s + sleep(1 - delta) # runtime = 46s # Fourth stage - All users are now at the desired number ts = time.time() From 32906d41b9f9a56c69da49a896cff61cdf88ea94 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:22:35 -0400 Subject: [PATCH 118/139] Add comments to explain complex logic in dispatch --- locust/dispatch.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/locust/dispatch.py b/locust/dispatch.py index b3722b02c2..ff2b478f89 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -138,6 +138,9 @@ def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(sel # The spawn rate greater than the remaining users to dispatch return True + # Workers having already more users than desired will show up zero + # users left to dispatch in the following dictionary. And this, even + # if these workers are missing users in one or more user classes. user_classes_count_left_to_dispatch_excluding_excess_users = { user_class: max( 0, @@ -147,6 +150,13 @@ def _desired_users_assignment_can_be_obtained_in_a_single_dispatch_iteration(sel for user_class in self._user_classes_count.keys() } + # This condition is to cover a corner case for which there exists no dispatch solution that won't + # violate the following constraints: + # - No worker run excess users at any point (except for the possible excess users already running) + # - No worker run excess users of any user class at any point (except for the + # possible excess users already running) + # - The total user count is never exceeded (except for the possible excess users already running) + # In a situation like this, we have no choice but to immediately dispatch the final users immediately. workers_user_count = self._workers_user_count if ( sum( From 8fd81f038e8e9049c9d65cb68d1c1cc976c0afbb Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 16:38:30 -0400 Subject: [PATCH 119/139] Swallow warnings in `clear_all_functools_lru_cache` helper --- locust/test/util.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/locust/test/util.py b/locust/test/util.py index a66edf62e9..2434bc7ccf 100644 --- a/locust/test/util.py +++ b/locust/test/util.py @@ -2,6 +2,7 @@ import gc import os import socket +import warnings from datetime import datetime, timedelta from cryptography import x509 @@ -64,10 +65,13 @@ def create_tls_cert(hostname): def clear_all_functools_lru_cache() -> None: - # Clear all `functools.lru_cache` to ensure that no state are persisted from one test to another. - # Taken from https://stackoverflow.com/a/50699209. - gc.collect() - wrappers = [a for a in gc.get_objects() if isinstance(a, functools._lru_cache_wrapper)] - assert len(wrappers) > 0 - for wrapper in wrappers: - wrapper.cache_clear() + # Somehow, the code below throws unrelated DeprecationWarning related to Flask. + # We mute the warnings in order to not pollute the logs when running the tests. + with warnings.catch_warnings(record=True): + # Clear all `functools.lru_cache` to ensure that no state are persisted from one test to another. + # Taken from https://stackoverflow.com/a/50699209. + gc.collect() + wrappers = [a for a in gc.get_objects() if isinstance(a, functools._lru_cache_wrapper)] + assert len(wrappers) > 0 + for wrapper in wrappers: + wrapper.cache_clear() From eb8c396c6719bc042e1ac22bf13ce0c6b93f0d10 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 25 Jun 2021 17:24:28 -0400 Subject: [PATCH 120/139] Delete uncovered code in dispatch --- locust/dispatch.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index ff2b478f89..4dbda40d49 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -197,15 +197,6 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) - if self._all_users_have_been_dispatched: - break - - if all( - self._dispatched_user_class_count(user_class) >= user_count - for user_class, user_count in self._user_classes_count.items() - ): - break - if self._dispatched_user_class_count(user_class_to_add) >= self._user_classes_count[user_class_to_add]: continue @@ -403,12 +394,6 @@ def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( if user_count < sum(self._desired_users_assigned_to_workers[worker_node_id].values()) } - if worker_node_id_to_add_user_on not in workers_user_count_without_excess_users: - return True - - if len(workers_user_count_without_excess_users) == 1: - return False - if workers_user_count_without_excess_users[worker_node_id_to_add_user_on] + 1 - min( workers_user_count.values() ) >= 2 and any( From cb0121ce252fc279b6f4040b3e123f70c355eb39 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 27 Jun 2021 18:21:46 -0400 Subject: [PATCH 121/139] [draft] Fix additional corner cases in dispatcher These corner cases would cause infinite loops in the dispatchers. A cleanup of the code will be done in an upcoming commit. --- locust/dispatch.py | 170 ++++++++++++++++- locust/test/test_dispatch.py | 353 +++++++++++++++++++++++++++++++++++ 2 files changed, 515 insertions(+), 8 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 4dbda40d49..7cba4ff650 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -110,7 +110,15 @@ def __init__(self, worker_nodes: "List[WorkerNode]", user_classes_count: Dict[st # Initialize the generator that is used in `__next__` self._dispatcher_generator = self._dispatcher() + self._iteration = 0 + + self._workers_desired_user_count = { + worker_node_id: sum(desired_users_on_worker.values()) + for worker_node_id, desired_users_on_worker in self._desired_users_assigned_to_workers.items() + } + def __next__(self) -> Dict[str, Dict[str, int]]: + self._iteration += 1 return next(self._dispatcher_generator) def _dispatcher(self) -> Generator[Dict[str, Dict[str, int]], None, None]: @@ -195,11 +203,37 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: # For large number of user classes and large number of workers, this assertion might fail. # If this happens, you can remove it or increase the threshold. Right now, the assertion # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). - assert i < 5000, "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) + assert i < 100 * len( + self._user_classes + ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) + + # if self._all_users_have_been_dispatched: + # break + + # if all( + # self._dispatched_user_class_count(user_class) >= user_count + # for user_class, user_count in self._user_classes_count.items() + # ): + # break + + if all(self._user_class_cant_be_assigned_to_any_worker(user_class) for user_class in self._user_classes): + # This means that we're at the last iteration of this dispatch cycle. If some user + # classes are in excess, this last iteration will stop those excess users. + self._dispatched_users.update(self._desired_users_assigned_to_workers) + self._users_left_to_assigned.update( + { + worker_node_id: {user_class: 0 for user_class in user_classes_count.keys()} + for worker_node_id, user_classes_count in self._dispatched_users.items() + } + ) + break if self._dispatched_user_class_count(user_class_to_add) >= self._user_classes_count[user_class_to_add]: continue + if self._user_class_cant_be_assigned_to_any_worker(user_class_to_add): + continue + if self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up(user_class_to_add): continue @@ -207,10 +241,10 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: assert j < int( 2 * self._number_of_workers ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) - if ( - self._dispatched_user_count == self._desired_user_count - or (self._user_count_per_dispatch - user_count_in_current_dispatch) - >= self._user_count_left_to_assigned + if self._worker_is_full(worker_node_id): + continue + if self._dispatched_user_count == self._desired_user_count or ( + self._user_count_per_dispatch - user_count_in_current_dispatch >= self._user_count_left_to_assigned ): # This means that we're at the last iteration of this dispatch cycle. If some user # classes are in excess, this last iteration will stop those excess users. @@ -304,6 +338,7 @@ def _adding_this_user_class_respects_distribution_better_than_adding_any_other_u not self._adding_this_user_class_respects_distribution(user_class) for user_class in self._user_classes_count.keys() if user_class != user_class_to_add + if not self._user_class_cant_be_assigned_to_any_worker(user_class) ): # If we are here, it means that if one user of `user_class_to_add` is added # then the distribution will be the best we can get. In other words, adding @@ -394,11 +429,23 @@ def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( if user_count < sum(self._desired_users_assigned_to_workers[worker_node_id].values()) } - if workers_user_count_without_excess_users[worker_node_id_to_add_user_on] + 1 - min( - workers_user_count.values() - ) >= 2 and any( + ideal_worker_on_which_to_add_user_exists = any( self._users_left_to_assigned[ideal_worker_node_id][user_class] > 0 for ideal_worker_node_id in ideal_worker_node_ids + ) + + if worker_node_id_to_add_user_on not in workers_user_count_without_excess_users: + return ideal_worker_on_which_to_add_user_exists + + # if len(workers_user_count_without_excess_users) == 1: + # return False + + if ( + workers_user_count_without_excess_users[worker_node_id_to_add_user_on] + + 1 + - min(workers_user_count.values()) + >= 2 + and ideal_worker_on_which_to_add_user_exists ): # Adding the user to the current worker will result in this worker having more than 1 # extra users compared to the other workers (condition on the left of the `and` above). @@ -425,6 +472,113 @@ def _dispatched_user_count(self) -> int: """Number of dispatched users at this time""" return sum(map(sum, map(dict.values, self._dispatched_users.values()))) + # @property + # def _all_workers_are_full(self) -> bool: + # return all(self._worker_is_full(worker_node.id) for worker_node in self._worker_nodes) + + def _worker_is_full(self, worker_node_id: str) -> bool: + return self._workers_user_count[worker_node_id] >= self._workers_desired_user_count[worker_node_id] + + # @property + # def _effective_user_count_left_to_dispatch(self) -> int: + # """This is the effective number of users left to dispatch. + # + # Say we have 3 user classes and 3 workers. The desired users are: + # + # { + # "Worker1": {"User1": 1, "User2": 0, "User3": 0}, + # "Worker2": {"User1": 0, "User2": 1, "User3": 0}, + # "Worker3": {"User1": 0, "User2": 0, "User3": 1}, + # } + # + # However, the workers are already running: + # + # { + # "Worker1": {"User1": 0, "User2": 0, "User3": 1}, + # "Worker2": {"User1": 1, "User2": 0, "User3": 0}, + # "Worker3": {"User1": 0, "User2": 1, "User3": 0}, + # } + # + # In this case, the effective number of users left to dispatch is 0 + # because we can't add a user to any of the workers without exceeding the + # maximum number of users that can run on a worker (which is 1 in this case). + # """ + # for a in self._users_left_to_assigned.items(): + # a = 1 + # for worker_node in self._worker_nodes: + # if self._worker_is_full(worker_node.id): + # continue + # for user_class, user_class_count in self._users_left_to_assigned[worker_node.id].items(): + # a = 1 + # + # effective_user_count_left_to_dispatch = 0 + # effective_users_left_to_dispatch = {} + # user_classes_count_to_add = defaultdict(lambda: 0) + # user_classes_count_to_stop = defaultdict(lambda: 0) + # for worker_node in self._worker_nodes: + # effective_users_left_to_dispatch[worker_node.id] = {} + # user_count_to_add_ = 0 + # user_count_to_stop_ = 0 + # for user_class in self._user_classes: + # difference = ( + # self._desired_users_assigned_to_workers[worker_node.id][user_class] + # - self._dispatched_users[worker_node.id][user_class] + # ) + # if difference > 0: + # effective_users_left_to_dispatch[worker_node.id][user_class] = difference + # user_count_to_add_ += difference + # user_classes_count_to_add[user_class] += difference + # elif difference < 0: + # effective_users_left_to_dispatch[worker_node.id][user_class] = difference + # user_count_to_stop_ += abs(difference) + # user_classes_count_to_stop[user_class] += abs(difference) + # effective_user_count_left_to_dispatch += user_count_to_add_ - user_count_to_stop_ + # + # effective_user_classes_count_to_add = user_classes_count_to_add.copy() + # for user_class, user_class_count_to_stop in user_classes_count_to_stop.items(): + # if user_class not in effective_user_classes_count_to_add: + # continue + # effective_user_classes_count_to_add[user_class] -= user_class_count_to_stop + # if effective_user_classes_count_to_add[user_class] <= 0: + # del effective_user_classes_count_to_add[user_class] + # + # for user_class, user_class_count in effective_user_classes_count_to_add.items(): + # for worker_node in self._worker_nodes: + # if self._worker_is_full(worker_node.id): + # continue + # if self._users_left_to_assigned[worker_node.id][user_class] == 0: + # continue + # a = 1 + # + # return effective_user_count_left_to_dispatch + + def _user_class_cant_be_assigned_to_any_worker(self, user_class_to_add: str) -> bool: + user_class_count_to_add = 0 + user_class_count_to_stop = 0 + for worker_node in self._worker_nodes: + difference = ( + self._desired_users_assigned_to_workers[worker_node.id][user_class_to_add] + - self._dispatched_users[worker_node.id][user_class_to_add] + ) + if difference > 0: + user_class_count_to_add += difference + elif difference < 0: + user_class_count_to_stop += abs(difference) + + effective_user_classes_count_to_add = user_class_count_to_add - user_class_count_to_stop + + if effective_user_classes_count_to_add <= 0: + return True + + for worker_node in self._worker_nodes: + if self._worker_is_full(worker_node.id): + continue + if self._users_left_to_assigned[worker_node.id][user_class_to_add] == 0: + continue + return False + + return True + class _WorkersUsersAssignor: """Helper to compute the users assigned to the workers""" diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 98b3c27fb9..519315e528 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -2089,6 +2089,359 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw delta = time.time() - ts self.assertTrue(0 <= delta <= 0.02, delta) + def test_dispatch_from_5_to_10_users_to_10_workers(self): + worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 11)] + worker_nodes[1].user_classes_count = {"TestUser6": 1} + worker_nodes[3].user_classes_count = {"TestUser4": 1} + worker_nodes[4].user_classes_count = {"TestUser5": 1} + worker_nodes[5].user_classes_count = {"TestUser10": 1} + worker_nodes[9].user_classes_count = {"TestUser1": 1} + + users_dispatcher = UsersDispatcher( + worker_nodes=worker_nodes, + user_classes_count={ + "TestUser1": 1, + "TestUser2": 1, + "TestUser3": 1, + "TestUser4": 1, + "TestUser5": 1, + "TestUser6": 1, + "TestUser7": 1, + "TestUser8": 1, + "TestUser9": 1, + "TestUser10": 1, + }, + spawn_rate=1, + ) + + list(users_dispatcher) + + sleep_time = 1 + + # TODO + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 0, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 0, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 0}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(0 <= delta <= 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 0, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 0}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 0}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 1, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 1, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 1, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 1, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 1}, + # "3": {"User1": 1, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + # delta = time.time() - ts + # self.assertTrue(0 <= delta <= 0.02, delta) + + def test_dispatch_from_20_to_55_users_to_10_workers(self): + worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 11)] + worker_nodes[0].user_classes_count = { + "TestUser1": 1, + "TestUser10": 1, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 0, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[1].user_classes_count = { + "TestUser1": 0, + "TestUser10": 1, + "TestUser2": 1, + "TestUser3": 0, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 0, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[2].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 1, + "TestUser3": 1, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 0, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[3].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 1, + "TestUser5": 1, + "TestUser6": 0, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[4].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 1, + "TestUser5": 1, + "TestUser6": 0, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[5].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 1, + "TestUser5": 1, + "TestUser6": 0, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[6].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 1, + "TestUser7": 1, + "TestUser8": 0, + "TestUser9": 0, + } + worker_nodes[7].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 1, + "TestUser7": 0, + "TestUser8": 1, + "TestUser9": 0, + } + worker_nodes[8].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 1, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 1, + } + worker_nodes[9].user_classes_count = { + "TestUser1": 0, + "TestUser10": 0, + "TestUser2": 0, + "TestUser3": 0, + "TestUser4": 0, + "TestUser5": 0, + "TestUser6": 1, + "TestUser7": 0, + "TestUser8": 0, + "TestUser9": 1, + } + + users_dispatcher = UsersDispatcher( + worker_nodes=worker_nodes, + user_classes_count={ + "TestUser1": 2, + "TestUser10": 5, + "TestUser2": 5, + "TestUser3": 3, + "TestUser4": 8, + "TestUser5": 10, + "TestUser6": 11, + "TestUser7": 4, + "TestUser8": 2, + "TestUser9": 5, + }, + spawn_rate=2, + ) + + list(users_dispatcher) + + sleep_time = 1 + + # TODO + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 0, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 0, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 0}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(0 <= delta <= 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 0, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 0}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 0}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 0, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 0, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 1, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 1, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 0}, + # "3": {"User1": 1, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertDictEqual( + # next(users_dispatcher), + # { + # "1": {"User1": 1, "User2": 1, "User3": 1}, + # "2": {"User1": 1, "User2": 1, "User3": 1}, + # "3": {"User1": 1, "User2": 1, "User3": 1}, + # }, + # ) + # delta = time.time() - ts + # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + # + # ts = time.time() + # self.assertRaises(StopIteration, lambda: next(users_dispatcher)) + # delta = time.time() - ts + # self.assertTrue(0 <= delta <= 0.02, delta) + def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: user_classes = list(next(iter(d.values())).keys()) From a5569c536056dfa84728e023c930a20dd8507f4b Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 16:01:19 -0400 Subject: [PATCH 122/139] Fix corner cases in dispatch logic causing infinite loops Also cleanup dispatch code and add comments --- locust/dispatch.py | 200 +++++---------- locust/test/test_dispatch.py | 480 ++++++++++++++++++++--------------- locust/test/test_runners.py | 142 +++++++++++ 3 files changed, 481 insertions(+), 341 deletions(-) diff --git a/locust/dispatch.py b/locust/dispatch.py index 7cba4ff650..650069882a 100644 --- a/locust/dispatch.py +++ b/locust/dispatch.py @@ -1,11 +1,13 @@ import functools import itertools +import json import math +import tempfile import time from collections.abc import Iterator from copy import deepcopy -from operator import itemgetter, methodcaller, ne -from typing import Dict, Generator, List, TYPE_CHECKING +from operator import contains, itemgetter, methodcaller, ne +from typing import Dict, Generator, List, TYPE_CHECKING, Tuple import gevent @@ -205,18 +207,11 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: # is there as a safeguard for situations that can't be easily tested (i.e. large scale distributed tests). assert i < 100 * len( self._user_classes - ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(i) - - # if self._all_users_have_been_dispatched: - # break - - # if all( - # self._dispatched_user_class_count(user_class) >= user_count - # for user_class, user_count in self._user_classes_count.items() - # ): - # break + ), "Looks like dispatch is stuck in an infinite loop (iteration {}). Crash dump:\n{}\nAlso written to {}".format( + i, *self._crash_dump() + ) - if all(self._user_class_cant_be_assigned_to_any_worker(user_class) for user_class in self._user_classes): + if all(self._user_class_cannot_be_assigned_to_any_worker(user_class) for user_class in self._user_classes): # This means that we're at the last iteration of this dispatch cycle. If some user # classes are in excess, this last iteration will stop those excess users. self._dispatched_users.update(self._desired_users_assigned_to_workers) @@ -231,7 +226,7 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: if self._dispatched_user_class_count(user_class_to_add) >= self._user_classes_count[user_class_to_add]: continue - if self._user_class_cant_be_assigned_to_any_worker(user_class_to_add): + if self._user_class_cannot_be_assigned_to_any_worker(user_class_to_add): continue if self._try_next_user_class_in_order_to_stay_balanced_during_ramp_up(user_class_to_add): @@ -240,7 +235,9 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: for j, worker_node_id in enumerate(itertools.cycle(sorted(self._users_left_to_assigned.keys()))): assert j < int( 2 * self._number_of_workers - ), "Looks like dispatch is stuck in an infinite loop (iteration {})".format(j) + ), "Looks like dispatch is stuck in an infinite loop (iteration {}). Crash dump:\n{}\nAlso written to {}".format( + j, *self._crash_dump() + ) if self._worker_is_full(worker_node_id): continue if self._dispatched_user_count == self._desired_user_count or ( @@ -276,6 +273,32 @@ def _users_to_dispatch_for_current_iteration(self) -> Dict[str, Dict[str, int]]: for worker_node_id, user_classes_count in sorted(self._dispatched_users.items(), key=itemgetter(0)) } + def _crash_dump(self) -> Tuple[str, str]: + """Parameters necessary to debug infinite loop issues. + + Users encountering an infinite loop issue should provide these informations. + """ + crash_dump = json.dumps( + { + "spawn_rate": self._spawn_rate, + "initial_dispatched_users": self._initial_dispatched_users, + "desired_users_assigned_to_workers": self._desired_users_assigned_to_workers, + "user_classes_count": self._user_classes_count, + "initial_user_count": sum(map(sum, map(dict.values, self._initial_dispatched_users.values()))), + "desired_user_count": sum(self._user_classes_count.values()), + "number_of_workers": self._number_of_workers, + }, + indent=" ", + ) + fp = tempfile.NamedTemporaryFile( + prefix="locust-dispatcher-crash-dump-", suffix=".json", mode="wt", delete=False + ) + try: + fp.write(crash_dump) + finally: + fp.close() + return crash_dump, fp.name + @property def _number_of_workers(self) -> int: return len(self._users_left_to_assigned) @@ -312,20 +335,23 @@ def _try_next_user_class_in_order_to_stay_balanced_during_ramp_up(self, user_cla else: # Because each user class doesn't have at least one running user, we use a simpler strategy - # that make sure each user class appears once. - for next_user_class in filter(functools.partial(ne, user_class_to_add), self._user_classes): - if sum(map(itemgetter(next_user_class), self._users_left_to_assigned.values())) == 0: - # No more users of class `next_user_class` to dispatch - continue + # that makes sure each user class appears once. + # + # The following code checks if another user class that would better preserves the distribution exists. + # If no such user class exists, this code will evaluate to `False` and the current user class + # will be considered as the next user class to be added to the pool of running users. + return any( + True + for next_user_class in filter(functools.partial(ne, user_class_to_add), self._user_classes) + if sum(map(itemgetter(next_user_class), self._users_left_to_assigned.values())) > 0 + if self._dispatched_user_class_count(next_user_class) < self._user_classes_count[next_user_class] + if not self._user_class_cannot_be_assigned_to_any_worker(next_user_class) if ( self._dispatched_user_classes_count()[user_class_to_add] - self._dispatched_user_classes_count()[next_user_class] >= 1 - ): - # There's already enough users for `user_class_to_add` in the current dispatch. Hence, we should - # not consider `user_class_to_add` and go to the next user class instead. - return True - return False + ) + ) def _adding_this_user_class_respects_distribution_better_than_adding_any_other_user_class( self, user_class_to_add: str @@ -338,7 +364,7 @@ def _adding_this_user_class_respects_distribution_better_than_adding_any_other_u not self._adding_this_user_class_respects_distribution(user_class) for user_class in self._user_classes_count.keys() if user_class != user_class_to_add - if not self._user_class_cant_be_assigned_to_any_worker(user_class) + if not self._user_class_cannot_be_assigned_to_any_worker(user_class) ): # If we are here, it means that if one user of `user_class_to_add` is added # then the distribution will be the best we can get. In other words, adding @@ -437,9 +463,6 @@ def _try_next_worker_in_order_to_stay_balanced_during_ramp_up( if worker_node_id_to_add_user_on not in workers_user_count_without_excess_users: return ideal_worker_on_which_to_add_user_exists - # if len(workers_user_count_without_excess_users) == 1: - # return False - if ( workers_user_count_without_excess_users[worker_node_id_to_add_user_on] + 1 @@ -472,113 +495,30 @@ def _dispatched_user_count(self) -> int: """Number of dispatched users at this time""" return sum(map(sum, map(dict.values, self._dispatched_users.values()))) - # @property - # def _all_workers_are_full(self) -> bool: - # return all(self._worker_is_full(worker_node.id) for worker_node in self._worker_nodes) - - def _worker_is_full(self, worker_node_id: str) -> bool: - return self._workers_user_count[worker_node_id] >= self._workers_desired_user_count[worker_node_id] - - # @property - # def _effective_user_count_left_to_dispatch(self) -> int: - # """This is the effective number of users left to dispatch. - # - # Say we have 3 user classes and 3 workers. The desired users are: - # - # { - # "Worker1": {"User1": 1, "User2": 0, "User3": 0}, - # "Worker2": {"User1": 0, "User2": 1, "User3": 0}, - # "Worker3": {"User1": 0, "User2": 0, "User3": 1}, - # } - # - # However, the workers are already running: - # - # { - # "Worker1": {"User1": 0, "User2": 0, "User3": 1}, - # "Worker2": {"User1": 1, "User2": 0, "User3": 0}, - # "Worker3": {"User1": 0, "User2": 1, "User3": 0}, - # } - # - # In this case, the effective number of users left to dispatch is 0 - # because we can't add a user to any of the workers without exceeding the - # maximum number of users that can run on a worker (which is 1 in this case). - # """ - # for a in self._users_left_to_assigned.items(): - # a = 1 - # for worker_node in self._worker_nodes: - # if self._worker_is_full(worker_node.id): - # continue - # for user_class, user_class_count in self._users_left_to_assigned[worker_node.id].items(): - # a = 1 - # - # effective_user_count_left_to_dispatch = 0 - # effective_users_left_to_dispatch = {} - # user_classes_count_to_add = defaultdict(lambda: 0) - # user_classes_count_to_stop = defaultdict(lambda: 0) - # for worker_node in self._worker_nodes: - # effective_users_left_to_dispatch[worker_node.id] = {} - # user_count_to_add_ = 0 - # user_count_to_stop_ = 0 - # for user_class in self._user_classes: - # difference = ( - # self._desired_users_assigned_to_workers[worker_node.id][user_class] - # - self._dispatched_users[worker_node.id][user_class] - # ) - # if difference > 0: - # effective_users_left_to_dispatch[worker_node.id][user_class] = difference - # user_count_to_add_ += difference - # user_classes_count_to_add[user_class] += difference - # elif difference < 0: - # effective_users_left_to_dispatch[worker_node.id][user_class] = difference - # user_count_to_stop_ += abs(difference) - # user_classes_count_to_stop[user_class] += abs(difference) - # effective_user_count_left_to_dispatch += user_count_to_add_ - user_count_to_stop_ - # - # effective_user_classes_count_to_add = user_classes_count_to_add.copy() - # for user_class, user_class_count_to_stop in user_classes_count_to_stop.items(): - # if user_class not in effective_user_classes_count_to_add: - # continue - # effective_user_classes_count_to_add[user_class] -= user_class_count_to_stop - # if effective_user_classes_count_to_add[user_class] <= 0: - # del effective_user_classes_count_to_add[user_class] - # - # for user_class, user_class_count in effective_user_classes_count_to_add.items(): - # for worker_node in self._worker_nodes: - # if self._worker_is_full(worker_node.id): - # continue - # if self._users_left_to_assigned[worker_node.id][user_class] == 0: - # continue - # a = 1 - # - # return effective_user_count_left_to_dispatch - - def _user_class_cant_be_assigned_to_any_worker(self, user_class_to_add: str) -> bool: - user_class_count_to_add = 0 - user_class_count_to_stop = 0 - for worker_node in self._worker_nodes: - difference = ( - self._desired_users_assigned_to_workers[worker_node.id][user_class_to_add] - - self._dispatched_users[worker_node.id][user_class_to_add] - ) - if difference > 0: - user_class_count_to_add += difference - elif difference < 0: - user_class_count_to_stop += abs(difference) - - effective_user_classes_count_to_add = user_class_count_to_add - user_class_count_to_stop - - if effective_user_classes_count_to_add <= 0: + def _user_class_cannot_be_assigned_to_any_worker(self, user_class_to_add: str) -> bool: + """No worker has enough place to accept this user class""" + effective_user_count_of_that_user_class_that_can_be_added = sum( + self._desired_users_assigned_to_workers[worker_node.id][user_class_to_add] + - self._dispatched_users[worker_node.id][user_class_to_add] + for worker_node in self._worker_nodes + ) + if effective_user_count_of_that_user_class_that_can_be_added <= 0: return True - for worker_node in self._worker_nodes: - if self._worker_is_full(worker_node.id): - continue - if self._users_left_to_assigned[worker_node.id][user_class_to_add] == 0: - continue + if any( + True + for worker_node in self._worker_nodes + if not self._worker_is_full(worker_node.id) + if self._users_left_to_assigned[worker_node.id][user_class_to_add] > 0 + ): return False return True + def _worker_is_full(self, worker_node_id: str) -> bool: + """The worker cannot accept more users without exceeding the maximum user count it can run""" + return self._workers_user_count[worker_node_id] >= self._workers_desired_user_count[worker_node_id] + class _WorkersUsersAssignor: """Helper to compute the users assigned to the workers""" diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 519315e528..c102f9b915 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -1,4 +1,5 @@ import itertools +import json import time import unittest from typing import Dict @@ -2090,6 +2091,10 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw self.assertTrue(0 <= delta <= 0.02, delta) def test_dispatch_from_5_to_10_users_to_10_workers(self): + """ + This test case was causing the dispatcher to go into an infinite loop. + The test is left to prevent future regressions. + """ worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 11)] worker_nodes[1].user_classes_count = {"TestUser6": 1} worker_nodes[3].user_classes_count = {"TestUser4": 1} @@ -2097,118 +2102,32 @@ def test_dispatch_from_5_to_10_users_to_10_workers(self): worker_nodes[5].user_classes_count = {"TestUser10": 1} worker_nodes[9].user_classes_count = {"TestUser1": 1} + user_classes_count = { + "TestUser1": 1, + "TestUser2": 1, + "TestUser3": 1, + "TestUser4": 1, + "TestUser5": 1, + "TestUser6": 1, + "TestUser7": 1, + "TestUser8": 1, + "TestUser9": 1, + "TestUser10": 1, + } + users_dispatcher = UsersDispatcher( - worker_nodes=worker_nodes, - user_classes_count={ - "TestUser1": 1, - "TestUser2": 1, - "TestUser3": 1, - "TestUser4": 1, - "TestUser5": 1, - "TestUser6": 1, - "TestUser7": 1, - "TestUser8": 1, - "TestUser9": 1, - "TestUser10": 1, - }, - spawn_rate=1, + worker_nodes=worker_nodes, user_classes_count=user_classes_count, spawn_rate=1 ) - list(users_dispatcher) - - sleep_time = 1 + dispatched_users = list(users_dispatcher) - # TODO - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 0, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 0, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 0}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(0 <= delta <= 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 0, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 0}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 0}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 1, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 1, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 1, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 1, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 1}, - # "3": {"User1": 1, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - # delta = time.time() - ts - # self.assertTrue(0 <= delta <= 0.02, delta) + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users[-1]), user_classes_count) def test_dispatch_from_20_to_55_users_to_10_workers(self): + """ + This test case was causing the dispatcher to go into an infinite loop. + The test is left to prevent future regressions. + """ worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 11)] worker_nodes[0].user_classes_count = { "TestUser1": 1, @@ -2331,116 +2250,255 @@ def test_dispatch_from_20_to_55_users_to_10_workers(self): "TestUser9": 1, } + user_classes_count = { + "TestUser1": 2, + "TestUser10": 5, + "TestUser2": 5, + "TestUser3": 3, + "TestUser4": 8, + "TestUser5": 10, + "TestUser6": 11, + "TestUser7": 4, + "TestUser8": 2, + "TestUser9": 5, + } + users_dispatcher = UsersDispatcher( - worker_nodes=worker_nodes, - user_classes_count={ - "TestUser1": 2, - "TestUser10": 5, - "TestUser2": 5, - "TestUser3": 3, - "TestUser4": 8, - "TestUser5": 10, - "TestUser6": 11, - "TestUser7": 4, - "TestUser8": 2, - "TestUser9": 5, - }, - spawn_rate=2, + worker_nodes=worker_nodes, user_classes_count=user_classes_count, spawn_rate=2 ) - list(users_dispatcher) + dispatched_users = list(users_dispatcher) - sleep_time = 1 + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users[-1]), user_classes_count) - # TODO - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 0, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 0, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 0}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(0 <= delta <= 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 0, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 0}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 0}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 0, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 0, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 1, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 1, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 0}, - # "3": {"User1": 1, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertDictEqual( - # next(users_dispatcher), - # { - # "1": {"User1": 1, "User2": 1, "User3": 1}, - # "2": {"User1": 1, "User2": 1, "User3": 1}, - # "3": {"User1": 1, "User2": 1, "User3": 1}, - # }, - # ) - # delta = time.time() - ts - # self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) - # - # ts = time.time() - # self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - # delta = time.time() - ts - # self.assertTrue(0 <= delta <= 0.02, delta) + def test_dispatch_from_38_to_61_users_to_10_workers(self): + """ + This test case was causing the dispatcher to go into an infinite loop. + The test is left to prevent future regressions. + """ + worker_nodes = [WorkerNode("0{}".format(i) if i < 10 else str(i)) for i in range(1, 11)] + worker_nodes[0].user_classes_count = { + "TestUser01": 1, + "TestUser02": 1, + "TestUser03": 1, + "TestUser05": 0, + "TestUser06": 0, + "TestUser07": 0, + "TestUser08": 0, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 1, + "TestUser14": 0, + "TestUser15": 0, + } + worker_nodes[1].user_classes_count = { + "TestUser01": 1, + "TestUser02": 1, + "TestUser03": 1, + "TestUser05": 0, + "TestUser06": 0, + "TestUser07": 0, + "TestUser08": 0, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 0, + "TestUser14": 1, + "TestUser15": 0, + } + worker_nodes[2].user_classes_count = { + "TestUser01": 0, + "TestUser02": 1, + "TestUser03": 1, + "TestUser05": 1, + "TestUser06": 0, + "TestUser07": 0, + "TestUser08": 0, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 0, + "TestUser14": 1, + "TestUser15": 0, + } + worker_nodes[3].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 1, + "TestUser05": 1, + "TestUser06": 1, + "TestUser07": 0, + "TestUser08": 0, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 0, + "TestUser14": 0, + "TestUser15": 1, + } + worker_nodes[4].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 0, + "TestUser05": 1, + "TestUser06": 1, + "TestUser07": 1, + "TestUser08": 0, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 0, + "TestUser14": 0, + "TestUser15": 1, + } + worker_nodes[5].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 0, + "TestUser05": 1, + "TestUser06": 0, + "TestUser07": 1, + "TestUser08": 1, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 0, + "TestUser14": 0, + "TestUser15": 1, + } + worker_nodes[6].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 0, + "TestUser05": 0, + "TestUser06": 0, + "TestUser07": 1, + "TestUser08": 1, + "TestUser09": 1, + "TestUser10": 0, + "TestUser11": 0, + "TestUser12": 0, + "TestUser14": 0, + "TestUser15": 1, + } + worker_nodes[7].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 0, + "TestUser05": 0, + "TestUser06": 0, + "TestUser07": 0, + "TestUser08": 1, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 1, + "TestUser12": 1, + "TestUser14": 0, + "TestUser15": 1, + } + worker_nodes[8].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 0, + "TestUser05": 0, + "TestUser06": 0, + "TestUser07": 0, + "TestUser08": 1, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 1, + "TestUser12": 1, + "TestUser14": 0, + "TestUser15": 0, + } + worker_nodes[9].user_classes_count = { + "TestUser01": 0, + "TestUser02": 0, + "TestUser03": 0, + "TestUser05": 0, + "TestUser06": 0, + "TestUser07": 0, + "TestUser08": 0, + "TestUser09": 0, + "TestUser10": 0, + "TestUser11": 1, + "TestUser12": 2, + "TestUser14": 0, + "TestUser15": 0, + } + + user_classes_count = { + "TestUser01": 3, + "TestUser02": 4, + "TestUser03": 7, + "TestUser05": 7, + "TestUser06": 3, + "TestUser07": 6, + "TestUser08": 6, + "TestUser09": 1, + "TestUser10": 0, + "TestUser11": 5, + "TestUser12": 7, + "TestUser14": 4, + "TestUser15": 8, + } + + users_dispatcher = UsersDispatcher( + worker_nodes=worker_nodes, user_classes_count=user_classes_count, spawn_rate=8.556688078766006 + ) + + dispatched_users = list(users_dispatcher) + + self.assertDictEqual(_aggregate_dispatched_users(dispatched_users[-1]), user_classes_count) + + def test_crash_dump(self): + worker_node1 = WorkerNode("1") + worker_node1.user_classes_count = {"User3": 1} + worker_node2 = WorkerNode("2") + worker_node2.user_classes_count = {} + worker_node3 = WorkerNode("3") + worker_node3.user_classes_count = {"User2": 4} + + users_dispatcher = UsersDispatcher( + worker_nodes=[worker_node1, worker_node2, worker_node3], + user_classes_count={"User1": 3, "User2": 3, "User3": 3}, + spawn_rate=0.7, + ) + + crash_dump_content, crash_dump_filepath = users_dispatcher._crash_dump() + + self.assertIsInstance(crash_dump_content, str) + self.assertIsInstance(crash_dump_filepath, str) + + crash_dumps = [json.loads(crash_dump_content)] + + with open(crash_dump_filepath, "rt") as f: + crash_dumps.append(json.load(f)) + + for crash_dump in crash_dumps: + self.assertEqual(len(crash_dump), 7) + self.assertEqual(crash_dump["spawn_rate"], 0.7) + self.assertDictEqual( + crash_dump["initial_dispatched_users"], + { + "1": {"User1": 0, "User2": 0, "User3": 1}, + "2": {"User1": 0, "User2": 0, "User3": 0}, + "3": {"User1": 0, "User2": 4, "User3": 0}, + }, + ) + self.assertDictEqual( + crash_dump["desired_users_assigned_to_workers"], + { + "1": {"User1": 1, "User2": 1, "User3": 1}, + "2": {"User1": 1, "User2": 1, "User3": 1}, + "3": {"User1": 1, "User2": 1, "User3": 1}, + }, + ) + self.assertDictEqual(crash_dump["user_classes_count"], {"User1": 3, "User2": 3, "User3": 3}) + self.assertEqual(crash_dump["initial_user_count"], 5) + self.assertEqual(crash_dump["desired_user_count"], 9) + self.assertEqual(crash_dump["number_of_workers"], 3) def _aggregate_dispatched_users(d: Dict[str, Dict[str, int]]) -> Dict[str, int]: diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 6a85f09668..d572b61ad9 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -957,6 +957,148 @@ def tick(self): master.stop() + @unittest.skip + def test_distributed_shape_fuzzy_test(self): + """ + Incredibility useful test to find issues with dispatch logic. This test allowed to find + multiple small corner cases with the new dispatch logic of locust v2. + + The test is disabled by default because it takes a lot of time to run and has randomness to it. + However, it is advised to run it a few times (you can run it in parallel) when modifying the dispatch logic. + """ + + class BaseUser(User): + @task + def my_task(self): + gevent.sleep(600) + + class TestUser01(BaseUser): + pass + + class TestUser02(BaseUser): + pass + + class TestUser03(BaseUser): + pass + + class TestUser04(BaseUser): + pass + + class TestUser05(BaseUser): + pass + + class TestUser06(BaseUser): + pass + + class TestUser07(BaseUser): + pass + + class TestUser08(BaseUser): + pass + + class TestUser09(BaseUser): + pass + + class TestUser10(BaseUser): + pass + + class TestUser11(BaseUser): + pass + + class TestUser12(BaseUser): + pass + + class TestUser13(BaseUser): + pass + + class TestUser14(BaseUser): + pass + + class TestUser15(BaseUser): + pass + + class TestShape(LoadTestShape): + def __init__(self): + super().__init__() + + self.stages = [] + runtime = 0 + for _ in range(100): + runtime += random.uniform(3, 15) + self.stages.append((runtime, random.randint(1, 100), random.uniform(0.1, 10))) + + def tick(self): + run_time = self.get_run_time() + for stage in self.stages: + if run_time < stage[0]: + return stage[1], stage[2] + + user_classes = [ + TestUser01, + TestUser02, + TestUser03, + TestUser04, + TestUser05, + TestUser06, + TestUser07, + TestUser08, + TestUser09, + TestUser10, + TestUser11, + TestUser12, + TestUser13, + TestUser14, + TestUser15, + ] + + chosen_user_classes = random.sample(user_classes, k=random.randint(1, len(user_classes))) + + for user_class in chosen_user_classes: + user_class.weight = random.uniform(1, 20) + + locust_worker_additional_wait_before_ready_after_stop = 5 + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3), _patch_env( + "LOCUST_WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", + str(locust_worker_additional_wait_before_ready_after_stop), + ): + stop_timeout = 5 + master_env = Environment( + user_classes=chosen_user_classes, shape_class=TestShape(), stop_timeout=stop_timeout + ) + master_env.shape_class.reset_time() + master = master_env.create_master_runner("*", 0) + + workers = [] + for i in range(random.randint(1, 30)): + worker_env = Environment(user_classes=chosen_user_classes) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) + workers.append(worker) + + # Give workers time to connect + sleep(0.1) + + self.assertEqual(STATE_INIT, master.state) + self.assertEqual(len(workers), len(master.clients.ready)) + + # Start a shape test + master.start_shape() + + ts = time.time() + while master.state != STATE_STOPPED: + self.assertTrue(time.time() - ts <= master_env.shape_class.stages[-1][0] + 60, master.state) + print( + "{:.2f}/{:.2f} | {} | {:.0f} | ".format( + time.time() - ts, + master_env.shape_class.stages[-1][0], + master.state, + sum(master.reported_user_classes_count.values()), + ) + + json.dumps(dict(sorted(master.reported_user_classes_count.items(), key=itemgetter(0)))) + ) + sleep(1) + + master.stop() + def test_distributed_shape_stop_and_restart(self): """ Test stopping and then restarting a LoadTestShape From 6a41004f31d4f311665f31d0461886ed5c70dbf0 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 16:01:37 -0400 Subject: [PATCH 123/139] Print master's state if assertion fails --- locust/test/test_runners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index d572b61ad9..551f30f414 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1199,7 +1199,7 @@ def tick(self): master.start_shape() while master.state != STATE_STOPPED: - self.assertTrue(time.perf_counter() - ts <= 40) + self.assertTrue(time.perf_counter() - ts <= 40, master.state) statuses.append((time.perf_counter() - ts, master.state, master.user_count)) sleep(0.1) From cf129caa04d6e4128ee02e13edd462fcdc5e41aa Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 16:02:16 -0400 Subject: [PATCH 124/139] Ensure that `/swarm` endpoint is not blocking --- locust/test/test_runners.py | 190 ++++++++++++++++++++++++++++++++++++ locust/web.py | 10 +- 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 551f30f414..718820ba52 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1,11 +1,15 @@ +import json import os +import random import time import unittest from collections import defaultdict from contextlib import contextmanager +from operator import itemgetter import gevent import mock +import requests from gevent import sleep from gevent.pool import Group from gevent.queue import Queue @@ -514,6 +518,88 @@ def on_custom_msg(msg, **kw): msg = self.mocked_log.warning[0] self.assertIn("Unknown message type recieved", msg) + def test_swarm_endpoint_is_non_blocking(self): + class TestUser1(User): + @task + def my_task(self): + gevent.sleep(600) + + class TestUser2(User): + @task + def my_task(self): + gevent.sleep(600) + + stop_timeout = 0 + env = Environment(user_classes=[TestUser1, TestUser2], stop_timeout=stop_timeout) + local_runner = env.create_local_runner() + web_ui = env.create_web_ui("127.0.0.1", 0) + + gevent.sleep(0.1) + + ts = time.perf_counter() + response = requests.post( + "http://127.0.0.1:{}/swarm".format(web_ui.server.server_port), + data={"user_count": 20, "spawn_rate": 1, "host": "https://localhost"}, + ) + self.assertEqual(200, response.status_code) + self.assertTrue(0 <= time.perf_counter() - ts <= 1, "swarm endpoint is blocking") + + ts = time.perf_counter() + while local_runner.state != STATE_RUNNING: + self.assertTrue(time.perf_counter() - ts <= 20, local_runner.state) + gevent.sleep(0.1) + + self.assertTrue(19 <= time.perf_counter() - ts <= 21) + + self.assertEqual(local_runner.user_count, 20) + + local_runner.stop() + web_ui.stop() + + def test_can_call_stop_endpoint_if_currently_swarming(self): + class TestUser1(User): + @task + def my_task(self): + gevent.sleep(600) + + class TestUser2(User): + @task + def my_task(self): + gevent.sleep(600) + + stop_timeout = 5 + env = Environment(user_classes=[TestUser1, TestUser2], stop_timeout=stop_timeout) + local_runner = env.create_local_runner() + web_ui = env.create_web_ui("127.0.0.1", 0) + + gevent.sleep(0.1) + + ts = time.perf_counter() + response = requests.post( + "http://127.0.0.1:{}/swarm".format(web_ui.server.server_port), + data={"user_count": 20, "spawn_rate": 1, "host": "https://localhost"}, + ) + self.assertEqual(200, response.status_code) + self.assertTrue(0 <= time.perf_counter() - ts <= 1, "swarm endpoint is blocking") + + gevent.sleep(5) + + self.assertEqual(local_runner.state, STATE_SPAWNING) + self.assertLessEqual(local_runner.user_count, 10) + + ts = time.perf_counter() + response = requests.get( + "http://127.0.0.1:{}/stop".format(web_ui.server.server_port), + ) + self.assertEqual(200, response.status_code) + self.assertTrue(stop_timeout <= time.perf_counter() - ts <= stop_timeout + 5, "stop endpoint took too long") + + self.assertEqual(local_runner.state, STATE_STOPPED) + self.assertLessEqual(local_runner.user_count, 0) + + local_runner.stop() + web_ui.stop() + class TestMasterWorkerRunners(LocustTestCase): def test_distributed_integration_run(self): @@ -1226,6 +1312,110 @@ def tick(self): elif state1 == STATE_RUNNING and state2 == STATE_STOPPED and stage == 3: self.assertTrue(30 - tolerance <= t2 <= 30 + tolerance) + def test_swarm_endpoint_is_non_blocking(self): + class TestUser1(User): + @task + def my_task(self): + gevent.sleep(600) + + class TestUser2(User): + @task + def my_task(self): + gevent.sleep(600) + + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + stop_timeout = 0 + master_env = Environment(user_classes=[TestUser1, TestUser2], stop_timeout=stop_timeout) + master = master_env.create_master_runner("*", 0) + web_ui = master_env.create_web_ui("127.0.0.1", 0) + + workers = [] + for i in range(2): + worker_env = Environment(user_classes=[TestUser1, TestUser2]) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) + workers.append(worker) + + # Give workers time to connect + sleep(0.1) + + self.assertEqual(STATE_INIT, master.state) + self.assertEqual(len(master.clients.ready), len(workers)) + + ts = time.perf_counter() + response = requests.post( + "http://127.0.0.1:{}/swarm".format(web_ui.server.server_port), + data={"user_count": 20, "spawn_rate": 1, "host": "https://localhost"}, + ) + self.assertEqual(200, response.status_code) + self.assertTrue(0 <= time.perf_counter() - ts <= 1, "swarm endpoint is blocking") + + ts = time.perf_counter() + while master.state != STATE_RUNNING: + self.assertTrue(time.perf_counter() - ts <= 20, master.state) + gevent.sleep(0.1) + + self.assertTrue(19 <= time.perf_counter() - ts <= 21) + + self.assertEqual(master.user_count, 20) + + master.stop() + web_ui.stop() + + def test_can_call_stop_endpoint_if_currently_swarming(self): + class TestUser1(User): + @task + def my_task(self): + gevent.sleep(600) + + class TestUser2(User): + @task + def my_task(self): + gevent.sleep(600) + + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + stop_timeout = 5 + master_env = Environment(user_classes=[TestUser1, TestUser2], stop_timeout=stop_timeout) + master = master_env.create_master_runner("*", 0) + web_ui = master_env.create_web_ui("127.0.0.1", 0) + + workers = [] + for i in range(2): + worker_env = Environment(user_classes=[TestUser1, TestUser2]) + worker = worker_env.create_worker_runner("127.0.0.1", master.server.port) + workers.append(worker) + + # Give workers time to connect + sleep(0.1) + + self.assertEqual(STATE_INIT, master.state) + self.assertEqual(len(master.clients.ready), len(workers)) + + ts = time.perf_counter() + response = requests.post( + "http://127.0.0.1:{}/swarm".format(web_ui.server.server_port), + data={"user_count": 20, "spawn_rate": 1, "host": "https://localhost"}, + ) + self.assertEqual(200, response.status_code) + self.assertTrue(0 <= time.perf_counter() - ts <= 1, "swarm endpoint is blocking") + + gevent.sleep(5) + + self.assertEqual(master.state, STATE_SPAWNING) + self.assertLessEqual(master.user_count, 10) + + ts = time.perf_counter() + response = requests.get( + "http://127.0.0.1:{}/stop".format(web_ui.server.server_port), + ) + self.assertEqual(200, response.status_code) + self.assertTrue(stop_timeout <= time.perf_counter() - ts <= stop_timeout + 5, "stop endpoint took too long") + + self.assertEqual(master.state, STATE_STOPPED) + self.assertLessEqual(master.user_count, 0) + + master.stop() + web_ui.stop() + class TestMasterRunner(LocustTestCase): def setUp(self): diff --git a/locust/web.py b/locust/web.py index ad0c053138..30ab6c9372 100644 --- a/locust/web.py +++ b/locust/web.py @@ -107,6 +107,7 @@ def __init__( self.app.config["BASIC_AUTH_ENABLED"] = False self.auth = None self.greenlet = None + self._swarm_greenlet = None if auth_credentials is not None: credentials = auth_credentials.split(":") @@ -150,12 +151,19 @@ def swarm(): user_count = int(request.form["user_count"]) spawn_rate = float(request.form["spawn_rate"]) - environment.runner.start(user_count, spawn_rate) + if self._swarm_greenlet is not None: + self._swarm_greenlet.kill(block=True) + self._swarm_greenlet = None + self._swarm_greenlet = gevent.spawn(environment.runner.start, user_count, spawn_rate) + self._swarm_greenlet.link_exception(greenlet_exception_handler) return jsonify({"success": True, "message": "Swarming started", "host": environment.host}) @app.route("/stop") @self.auth_required_if_enabled def stop(): + if self._swarm_greenlet is not None: + self._swarm_greenlet.kill(block=True) + self._swarm_greenlet = None environment.runner.stop() return jsonify({"success": True, "message": "Test stopped"}) From c9acba0a15792569e742dc140f5cb31ad71df610 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 21:34:55 -0400 Subject: [PATCH 125/139] Attempt at reducing test flakyness --- locust/test/test_runners.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index 718820ba52..d8d7cf9329 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -594,7 +594,11 @@ def my_task(self): self.assertEqual(200, response.status_code) self.assertTrue(stop_timeout <= time.perf_counter() - ts <= stop_timeout + 5, "stop endpoint took too long") - self.assertEqual(local_runner.state, STATE_STOPPED) + ts = time.perf_counter() + while local_runner.state != STATE_STOPPED: + self.assertTrue(time.perf_counter() - ts <= 2) + gevent.sleep(0.1) + self.assertLessEqual(local_runner.user_count, 0) local_runner.stop() @@ -1285,7 +1289,7 @@ def tick(self): master.start_shape() while master.state != STATE_STOPPED: - self.assertTrue(time.perf_counter() - ts <= 40, master.state) + self.assertTrue(time.perf_counter() - ts <= 45, master.state) statuses.append((time.perf_counter() - ts, master.state, master.user_count)) sleep(0.1) @@ -1410,7 +1414,11 @@ def my_task(self): self.assertEqual(200, response.status_code) self.assertTrue(stop_timeout <= time.perf_counter() - ts <= stop_timeout + 5, "stop endpoint took too long") - self.assertEqual(master.state, STATE_STOPPED) + ts = time.perf_counter() + while master.state != STATE_STOPPED: + self.assertTrue(time.perf_counter() - ts <= 2) + gevent.sleep(0.1) + self.assertLessEqual(master.user_count, 0) master.stop() From 0d9a238e4e52da168bed164edb1dbd4a47cf7e7e Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 22:13:33 -0400 Subject: [PATCH 126/139] Attempt at reducing test flakyness --- locust/test/test_dispatch.py | 248 ++++++++++++++++++----------------- locust/test/test_runners.py | 16 ++- 2 files changed, 137 insertions(+), 127 deletions(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index c102f9b915..4f2eb07d5e 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -8,6 +8,8 @@ from locust.runners import WorkerNode from locust.test.util import clear_all_functools_lru_cache +_TOLERANCE = 0.05 + class TestAssignUsersToWorkers(unittest.TestCase): def test_assign_users_to_1_worker(self): @@ -296,7 +298,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -308,7 +310,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -320,7 +322,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -332,7 +334,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -344,7 +346,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -356,7 +358,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -368,7 +370,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -380,7 +382,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -392,12 +394,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -422,7 +424,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -434,7 +436,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -446,7 +448,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -458,7 +460,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -470,7 +472,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -482,7 +484,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -494,7 +496,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -506,7 +508,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -518,12 +520,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -548,7 +550,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -560,7 +562,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -572,7 +574,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -584,7 +586,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -596,12 +598,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") @@ -626,7 +628,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -638,7 +640,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -650,7 +652,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -662,7 +664,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -674,12 +676,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -704,7 +706,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -716,7 +718,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -728,12 +730,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -758,7 +760,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -770,7 +772,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -782,12 +784,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -810,12 +812,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) class TestDispatchUsersToWorkersHavingLessUsersThanTheTarget(unittest.TestCase): @@ -845,7 +847,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -857,7 +859,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -869,7 +871,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -881,7 +883,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -893,7 +895,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -905,7 +907,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -917,12 +919,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): worker_node1 = WorkerNode("1") @@ -950,7 +952,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -962,7 +964,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -974,7 +976,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -986,7 +988,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -998,7 +1000,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1010,7 +1012,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1022,12 +1024,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): worker_node1 = WorkerNode("1") @@ -1055,7 +1057,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1067,7 +1069,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1079,7 +1081,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1091,12 +1093,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): worker_node1 = WorkerNode("1") @@ -1124,7 +1126,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1136,7 +1138,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1148,7 +1150,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1160,12 +1162,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -1193,7 +1195,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1205,7 +1207,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1217,12 +1219,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): worker_node1 = WorkerNode("1") @@ -1250,7 +1252,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1262,12 +1264,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): worker_node1 = WorkerNode("1") @@ -1293,12 +1295,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): worker_node1 = WorkerNode("1") @@ -1332,7 +1334,7 @@ def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1346,12 +1348,12 @@ def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) class TestDispatchUsersToWorkersHavingLessAndMoreUsersThanTheTargetAndMoreTotalUsers(unittest.TestCase): @@ -1388,12 +1390,12 @@ def test_dispatch_users_to_3_workers(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1432,7 +1434,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_one_worker_empty }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1444,12 +1446,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_one_worker_empty }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(self): worker_node1 = WorkerNode("1") @@ -1477,7 +1479,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(s }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1489,12 +1491,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(s }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_empty(self): worker_node1 = WorkerNode("1") @@ -1520,12 +1522,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_empty(self): worker_node1 = WorkerNode("1") @@ -1551,12 +1553,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_empty_and_one_user_class_requiring_no_change( self, @@ -1586,7 +1588,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1598,12 +1600,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_empty_and_one_user_class_requiring_no_change( self, @@ -1633,7 +1635,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertDictEqual( @@ -1645,12 +1647,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_greater_than_or_equal_to_2(self): worker_node1_user_classes_count_cases = [{}, {"User3": 1}, {"User3": 2}] @@ -1685,12 +1687,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_greater_than_or_equal_to_2( }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1733,12 +1735,12 @@ def test_dispatch_users_to_3_workers(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1769,12 +1771,12 @@ def test_dispatch_users_to_3_workers(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1814,7 +1816,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) # total user count = 10 ts = time.time() @@ -1830,7 +1832,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 15 ts = time.time() @@ -1846,7 +1848,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 20 ts = time.time() @@ -1862,7 +1864,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 25 ts = time.time() @@ -1878,7 +1880,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 30 ts = time.time() @@ -1894,7 +1896,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 35 ts = time.time() @@ -1910,7 +1912,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 40 ts = time.time() @@ -1926,7 +1928,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 45 ts = time.time() @@ -1942,7 +1944,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 50 ts = time.time() @@ -1958,7 +1960,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 55 ts = time.time() @@ -1974,7 +1976,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 60 ts = time.time() @@ -1990,7 +1992,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 65 ts = time.time() @@ -2006,7 +2008,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 70 ts = time.time() @@ -2022,7 +2024,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 75, User1 = 25, User2 = 50 ts = time.time() @@ -2038,12 +2040,12 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): }, ) delta = time.time() - ts - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) class TestDispatch(unittest.TestCase): @@ -2081,14 +2083,14 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 25 + dispatch_iteration + 1}) delta = time.time() - ts if dispatch_iteration == 0: - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) else: - self.assertTrue(sleep_time - 0.02 <= delta <= sleep_time + 0.02, delta) + self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) ts = time.time() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) delta = time.time() - ts - self.assertTrue(0 <= delta <= 0.02, delta) + self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_from_5_to_10_users_to_10_workers(self): """ diff --git a/locust/test/test_runners.py b/locust/test/test_runners.py index d8d7cf9329..181c656984 100644 --- a/locust/test/test_runners.py +++ b/locust/test/test_runners.py @@ -1264,7 +1264,11 @@ def tick(self): else: return None - with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3): + locust_worker_additional_wait_before_ready_after_stop = 2 + with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3), _patch_env( + "LOCUST_WORKER_ADDITIONAL_WAIT_BEFORE_READY_AFTER_STOP", + str(locust_worker_additional_wait_before_ready_after_stop), + ): stop_timeout = 0 master_env = Environment(user_classes=[TestUser1], shape_class=TestShape(), stop_timeout=stop_timeout) master_env.shape_class.reset_time() @@ -1289,7 +1293,11 @@ def tick(self): master.start_shape() while master.state != STATE_STOPPED: - self.assertTrue(time.perf_counter() - ts <= 45, master.state) + # +5s buffer to let master stop + self.assertTrue( + time.perf_counter() - ts <= 30 + locust_worker_additional_wait_before_ready_after_stop + 5, + master.state, + ) statuses.append((time.perf_counter() - ts, master.state, master.user_count)) sleep(0.1) @@ -2963,7 +2971,7 @@ class MyTestUser(User): runner.spawning_greenlet.join() delta = time.time() - ts self.assertTrue( - 0 <= delta <= 0.01, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta + 0 <= delta <= 0.05, "Expected user count to increase to 10 instantaneously, instead it took %f" % delta ) self.assertTrue( runner.user_count == 10, "User count has not decreased correctly to 2, it is : %i" % runner.user_count @@ -2973,7 +2981,7 @@ class MyTestUser(User): runner.start(2, 4, wait=False) runner.spawning_greenlet.join() delta = time.time() - ts - self.assertTrue(1 <= delta <= 1.01, "Expected user count to decrease to 2 in 1s, instead it took %f" % delta) + self.assertTrue(1 <= delta <= 1.05, "Expected user count to decrease to 2 in 1s, instead it took %f" % delta) self.assertTrue( runner.user_count == 2, "User count has not decreased correctly to 2, it is : %i" % runner.user_count ) From 3e4f09dee6fb506a7aa35562e15b0baee069b02f Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 22:22:57 -0400 Subject: [PATCH 127/139] Set dispatcher tolerance to 25ms --- locust/test/test_dispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index 4f2eb07d5e..c2abbef3aa 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -8,7 +8,7 @@ from locust.runners import WorkerNode from locust.test.util import clear_all_functools_lru_cache -_TOLERANCE = 0.05 +_TOLERANCE = 0.025 class TestAssignUsersToWorkers(unittest.TestCase): From 7ca69b8320bf9de22a99b9db34ea0e0db6e144e8 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Mon, 28 Jun 2021 22:25:05 -0400 Subject: [PATCH 128/139] Use perf counter in dispatch tests --- locust/test/test_dispatch.py | 488 +++++++++++++++++------------------ 1 file changed, 244 insertions(+), 244 deletions(-) diff --git a/locust/test/test_dispatch.py b/locust/test/test_dispatch.py index c2abbef3aa..9365722b5b 100644 --- a/locust/test/test_dispatch.py +++ b/locust/test/test_dispatch.py @@ -288,7 +288,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): sleep_time = 1 / 0.5 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -297,10 +297,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -309,10 +309,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -321,10 +321,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -333,10 +333,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -345,10 +345,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -357,10 +357,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -369,10 +369,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 1, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -381,10 +381,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -393,12 +393,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): @@ -414,7 +414,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -423,10 +423,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -435,10 +435,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -447,10 +447,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -459,10 +459,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -471,10 +471,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -483,10 +483,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -495,10 +495,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 1, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -507,10 +507,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -519,12 +519,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): @@ -540,7 +540,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -549,10 +549,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -561,10 +561,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -573,10 +573,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -585,10 +585,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -597,12 +597,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): @@ -618,7 +618,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): sleep_time = 2 / 2.4 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -627,10 +627,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 0, "User2": 0, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -639,10 +639,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -651,10 +651,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -663,10 +663,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -675,12 +675,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): @@ -696,7 +696,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -705,10 +705,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -717,10 +717,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -729,12 +729,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): @@ -750,7 +750,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -759,10 +759,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): "3": {"User1": 0, "User2": 0, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -771,10 +771,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -783,12 +783,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): @@ -802,7 +802,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): spawn_rate=9, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -811,12 +811,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) @@ -837,7 +837,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): sleep_time = 1 / 0.5 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -846,10 +846,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -858,10 +858,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -870,10 +870,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -882,10 +882,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -894,10 +894,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -906,10 +906,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -918,12 +918,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): @@ -942,7 +942,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -951,10 +951,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -963,10 +963,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -975,10 +975,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -987,10 +987,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -999,10 +999,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1011,10 +1011,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1023,12 +1023,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): @@ -1047,7 +1047,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1056,10 +1056,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1068,10 +1068,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1080,10 +1080,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1092,12 +1092,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): @@ -1116,7 +1116,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): sleep_time = 2 / 2.4 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1125,10 +1125,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1137,10 +1137,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1149,10 +1149,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1161,12 +1161,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_2_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): @@ -1185,7 +1185,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1194,10 +1194,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): "3": {"User1": 0, "User2": 1, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1206,10 +1206,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1218,12 +1218,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_3(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): @@ -1242,7 +1242,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1251,10 +1251,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): "3": {"User1": 0, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1263,12 +1263,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_4(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): @@ -1285,7 +1285,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): spawn_rate=9, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1294,12 +1294,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_9(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): @@ -1322,7 +1322,7 @@ def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1333,10 +1333,10 @@ def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): "5": {"User1": 1, "User2": 0, "User3": 2}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1347,12 +1347,12 @@ def test_dispatch_users_to_5_workers_with_spawn_rate_of_3(self): "5": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) @@ -1380,7 +1380,7 @@ def test_dispatch_users_to_3_workers(self): spawn_rate=spawn_rate, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1389,12 +1389,12 @@ def test_dispatch_users_to_3_workers(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1424,7 +1424,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_one_worker_empty sleep_time = 1 / 0.5 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1433,10 +1433,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_one_worker_empty "3": {"User1": 0, "User2": 4, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1445,12 +1445,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_one_worker_empty "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(self): @@ -1469,7 +1469,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(s sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1478,10 +1478,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(s "3": {"User1": 0, "User2": 4, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1490,12 +1490,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_one_worker_empty(s "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_empty(self): @@ -1512,7 +1512,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ spawn_rate=0.5, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1521,12 +1521,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_empty(self): @@ -1543,7 +1543,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em spawn_rate=1, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1552,12 +1552,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_empty_and_one_user_class_requiring_no_change( @@ -1578,7 +1578,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ sleep_time = 1 / 0.5 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1587,10 +1587,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ "3": {"User1": 1, "User2": 4, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1599,12 +1599,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_0_5_and_all_workers_non_ "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_empty_and_one_user_class_requiring_no_change( @@ -1625,7 +1625,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em sleep_time = 1 - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1634,10 +1634,10 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em "3": {"User1": 1, "User2": 4, "User3": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1646,12 +1646,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_of_1_and_all_workers_non_em "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_users_to_3_workers_with_spawn_rate_greater_than_or_equal_to_2(self): @@ -1677,7 +1677,7 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_greater_than_or_equal_to_2( spawn_rate=spawn_rate, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1686,12 +1686,12 @@ def test_dispatch_users_to_3_workers_with_spawn_rate_greater_than_or_equal_to_2( "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1725,7 +1725,7 @@ def test_dispatch_users_to_3_workers(self): spawn_rate=spawn_rate, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1734,12 +1734,12 @@ def test_dispatch_users_to_3_workers(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1761,7 +1761,7 @@ def test_dispatch_users_to_3_workers(self): spawn_rate=spawn_rate, ) - ts = time.time() + ts = time.perf_counter() self.assertDictEqual( next(users_dispatcher), { @@ -1770,12 +1770,12 @@ def test_dispatch_users_to_3_workers(self): "3": {"User1": 1, "User2": 1, "User3": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) clear_all_functools_lru_cache() @@ -1803,7 +1803,7 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): sleep_time = 1 # total user count = 5 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 2, "User2": 3}) self.assertDictEqual( @@ -1815,11 +1815,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 1, "User2": 0}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) # total user count = 10 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 4, "User2": 6}) self.assertDictEqual( @@ -1831,11 +1831,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 1, "User2": 1}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 15 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 5, "User2": 10}) self.assertDictEqual( @@ -1847,11 +1847,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 1, "User2": 2}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 20 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 7, "User2": 13}) self.assertDictEqual( @@ -1863,11 +1863,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 2, "User2": 3}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 25 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 9, "User2": 16}) self.assertDictEqual( @@ -1879,11 +1879,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 2, "User2": 4}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 30 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 10, "User2": 20}) self.assertDictEqual( @@ -1895,11 +1895,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 3, "User2": 4}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 35 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 12, "User2": 23}) self.assertDictEqual( @@ -1911,11 +1911,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 3, "User2": 5}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 40 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 14, "User2": 26}) self.assertDictEqual( @@ -1927,11 +1927,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 4, "User2": 6}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 45 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 15, "User2": 30}) self.assertDictEqual( @@ -1943,11 +1943,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 4, "User2": 7}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 50 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 17, "User2": 33}) self.assertDictEqual( @@ -1959,11 +1959,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 4, "User2": 8}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 55 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 19, "User2": 36}) self.assertDictEqual( @@ -1975,11 +1975,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 5, "User2": 8}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 60 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 20, "User2": 40}) self.assertDictEqual( @@ -1991,11 +1991,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 5, "User2": 10}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 65 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 22, "User2": 43}) self.assertDictEqual( @@ -2007,11 +2007,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 6, "User2": 10}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 70 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 24, "User2": 46}) self.assertDictEqual( @@ -2023,11 +2023,11 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 6, "User2": 11}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) # total user count = 75, User1 = 25, User2 = 50 - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 25, "User2": 50}) self.assertDictEqual( @@ -2039,12 +2039,12 @@ def test_dispatch_75_users_to_4_workers_with_spawn_rate_of_5(self): "4": {"User1": 6, "User2": 12}, }, ) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) @@ -2078,18 +2078,18 @@ def test_dispatch_50_total_users_with_25_already_running_to_20_workers_with_spaw sleep_time = 1 for dispatch_iteration in range(25): - ts = time.time() + ts = time.perf_counter() dispatched_users = next(users_dispatcher) self.assertDictEqual(_aggregate_dispatched_users(dispatched_users), {"User1": 25 + dispatch_iteration + 1}) - delta = time.time() - ts + delta = time.perf_counter() - ts if dispatch_iteration == 0: self.assertTrue(0 <= delta <= _TOLERANCE, delta) else: self.assertTrue(sleep_time - _TOLERANCE <= delta <= sleep_time + _TOLERANCE, delta) - ts = time.time() + ts = time.perf_counter() self.assertRaises(StopIteration, lambda: next(users_dispatcher)) - delta = time.time() - ts + delta = time.perf_counter() - ts self.assertTrue(0 <= delta <= _TOLERANCE, delta) def test_dispatch_from_5_to_10_users_to_10_workers(self): From 7cad47539c3f6c47df7b266305d52e58137b76b4 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 29 Jun 2021 09:31:47 -0400 Subject: [PATCH 129/139] Use free random port for zrpc test --- locust/test/test_zmqrpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locust/test/test_zmqrpc.py b/locust/test/test_zmqrpc.py index 4b9d9428f9..dc2f8b314e 100644 --- a/locust/test/test_zmqrpc.py +++ b/locust/test/test_zmqrpc.py @@ -3,6 +3,7 @@ from locust.rpc import zmqrpc, Message from locust.test.testcases import LocustTestCase from locust.exception import RPCError +from locust.test.util import get_free_tcp_port class ZMQRPC_tests(LocustTestCase): @@ -46,9 +47,10 @@ def test_client_retry(self): server.recv_from_client() def test_rpc_error(self): - server = zmqrpc.Server("127.0.0.1", 5557) + port = get_free_tcp_port() + server = zmqrpc.Server("127.0.0.1", port) with self.assertRaises(RPCError): - server = zmqrpc.Server("127.0.0.1", 5557) + server = zmqrpc.Server("127.0.0.1", port) server.close() with self.assertRaises(RPCError): server.send_to_client(Message("test", "message", "identity")) From 167643676c0f19e7d7407dfec384297a64c352fb Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Tue, 29 Jun 2021 09:42:21 -0400 Subject: [PATCH 130/139] Do not use `get_free_tcp_port` for zmqrpc test --- locust/test/test_zmqrpc.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/locust/test/test_zmqrpc.py b/locust/test/test_zmqrpc.py index dc2f8b314e..abaa8144ad 100644 --- a/locust/test/test_zmqrpc.py +++ b/locust/test/test_zmqrpc.py @@ -3,7 +3,6 @@ from locust.rpc import zmqrpc, Message from locust.test.testcases import LocustTestCase from locust.exception import RPCError -from locust.test.util import get_free_tcp_port class ZMQRPC_tests(LocustTestCase): @@ -47,10 +46,9 @@ def test_client_retry(self): server.recv_from_client() def test_rpc_error(self): - port = get_free_tcp_port() - server = zmqrpc.Server("127.0.0.1", port) + server = zmqrpc.Server("127.0.0.1", 0) with self.assertRaises(RPCError): - server = zmqrpc.Server("127.0.0.1", port) + server = zmqrpc.Server("127.0.0.1", server.port) server.close() with self.assertRaises(RPCError): server.send_to_client(Message("test", "message", "identity")) From cb585b4682e472e91efe9e1c6a0518dad51c9a88 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Thu, 1 Jul 2021 12:47:54 -0400 Subject: [PATCH 131/139] Fix outdated docstring --- locust/user/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locust/user/users.py b/locust/user/users.py index 6f5221473a..7ef311bac6 100644 --- a/locust/user/users.py +++ b/locust/user/users.py @@ -153,7 +153,7 @@ def start(self, group: Group): Start a greenlet that runs this User instance. :param group: Group instance where the greenlet will be spawned. - :type gevent_group: gevent.pool.Group + :type group: gevent.pool.Group :returns: The spawned greenlet. """ From 5379f9e4dafceca2186304ae132776c647192881 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 2 Jul 2021 15:28:10 -0400 Subject: [PATCH 132/139] Add TODO regarding ill-defined load test shape --- locust/runners.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 3e023cebf4..97187c65db 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -381,6 +381,19 @@ def shape_worker(self): else: user_count, spawn_rate = new_state logger.info("Shape test updating to %d users at %.2f spawn rate" % (user_count, spawn_rate)) + # TODO: This `self.start()` call is blocking until the ramp-up is completed. This can leads + # to unexpected behaviours such as the one in the following example: + # A load test shape has the following stages: + # stage 1: (user_count=100, spawn_rate=1) for t < 50s + # stage 2: (user_count=120, spawn_rate=1) for t < 100s + # stage 3: (user_count=130, spawn_rate=1) for t < 120s + # Because the first stage will take 100s to complete, the second stage + # will be skipped completely because the shape worker will be blocked + # at the `self.start()` of the first stage. + # Of couse, this isn't a problem if the load test shape is well-defined. + # We should probably use a `gevent.timeout` with a duration a little over + # `(user_count - prev_user_count) / spawn_rate` in order to limit the runtime + # of each load test shape stage. self.start(user_count=user_count, spawn_rate=spawn_rate) self.shape_last_state = new_state From a96f403cabd1c83e778e4f819b32a8556a2535bc Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 2 Jul 2021 15:29:34 -0400 Subject: [PATCH 133/139] Add TODO regarding args missing from greenlet --- locust/runners.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 97187c65db..82cb824b3f 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -150,9 +150,15 @@ def user_classes_count(self) -> Dict[str, int]: try: user = user_greenlet.args[0] except IndexError: - logger.error( - "While calculating number of running users, we encountered a user that didnt have proper args %s", + # TODO: Find out why args is sometimes empty. In gevent code, + # the supplied args are cleared in the gevent.greenlet.Greenlet.__free, + # so it seems a good place to start investigating. My suspicion is that + # the supplied args are emptied whenever the greenlet is dead, so we can + # simply ignore the greenlets with empty args. + logger.debug( + "ERROR: While calculating number of running users, we encountered a user that didnt have proper args %s (user_greenlet.dead=%s)", user_greenlet, + user_greenlet.dead, ) continue user_classes_count[user.__class__.__name__] += 1 From 04ff90fafc3045d484834eb4474e0630f4b6ca75 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 2 Jul 2021 15:30:51 -0400 Subject: [PATCH 134/139] Make sure `host` is set on the user_class of the workers When master sends a non-null `host` to the workers, set this `host` for the users. --- locust/runners.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/locust/runners.py b/locust/runners.py index 82cb824b3f..3d6d9c707b 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -1021,6 +1021,10 @@ def start_worker(self, user_classes_count: Dict[str, int], **kwargs): self.cpu_warning_emitted = False self.worker_cpu_warning_emitted = False + for user_class in self.user_classes: + if self.environment.host is not None: + user_class.host = self.environment.host + user_classes_spawn_count = {} user_classes_stop_count = {} From 2cf679faa1b7cfccd459a67be055c88f1c18bdf5 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Fri, 2 Jul 2021 15:39:11 -0400 Subject: [PATCH 135/139] Refactor logging to be less verbose when level is INFO Other logging related code was also refactored to be cleaner and/or be more exact. --- locust/runners.py | 90 +++++++++++++++++++--------------------- locust/test/test_main.py | 8 ++-- 2 files changed, 46 insertions(+), 52 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 3d6d9c707b..f58e00f292 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -10,6 +10,10 @@ import traceback from collections import defaultdict from collections.abc import MutableMapping +from operator import ( + itemgetter, + methodcaller, +) from typing import ( Dict, Iterator, @@ -186,16 +190,10 @@ def spawn_users(self, user_classes_spawn_count: Dict[str, int], wait: bool = Fal if self.state == STATE_INIT or self.state == STATE_STOPPED: self.update_state(STATE_SPAWNING) - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - "Spawning additional %s (%s already running)..." - % (json.dumps(user_classes_spawn_count), json.dumps(self.user_classes_count)) - ) - elif sum(user_classes_spawn_count.values()) > 0: - logger.info( - "Spawning additional %s (%s already running)..." - % (sum(user_classes_spawn_count.values()), sum(self.user_classes_count.values())) - ) + logger.debug( + "Spawning additional %s (%s already running)..." + % (json.dumps(user_classes_spawn_count), json.dumps(self.user_classes_count)) + ) def spawn(user_class: str, spawn_count: int): n = 0 @@ -262,11 +260,9 @@ def stop_users(self, user_classes_stop_count: Dict[str, int]): ) stop_group.kill(block=True) - msg = "%i Users have been stopped, %g still running" % (sum(user_classes_stop_count.values()), self.user_count) - if logger.isEnabledFor(logging.DEBUG): - logger.debug(msg) - elif sum(user_classes_stop_count.values()) > 0: - logger.info(msg) + logger.debug( + "%g users have been stopped, %g still running", sum(user_classes_stop_count.values()), self.user_count + ) def monitor_cpu(self): process = psutil.Process() @@ -308,11 +304,10 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): local_worker_node.user_classes_count = self.user_classes_count if self.state != STATE_INIT and self.state != STATE_STOPPED: - logger.debug( - "Updating running test with %d users, %.2f spawn rate and wait=%r" % (user_count, spawn_rate, wait) - ) self.update_state(STATE_SPAWNING) + logger.info("Updating test with %d users, %.2f spawn rate and wait=%r" % (user_count, spawn_rate, wait)) + try: for dispatched_users in UsersDispatcher( worker_nodes=[local_worker_node], @@ -322,12 +317,8 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): user_classes_spawn_count = {} user_classes_stop_count = {} user_classes_count = dispatched_users[local_worker_node.id] - logger.info("Updating running test with %d users" % (sum(user_classes_count.values()),)) + logger.debug("Updating running test with %s" % _format_user_classes_count_for_log(user_classes_count)) for user_class, user_class_count in user_classes_count.items(): - logger.debug( - "Updating running test with %d users of class %s and wait=%r" - % (user_class_count, user_class, wait) - ) if self.user_classes_count[user_class] > user_class_count: user_classes_stop_count[user_class] = self.user_classes_count[user_class] - user_class_count elif self.user_classes_count[user_class] < user_class_count: @@ -348,13 +339,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): # a gevent.sleep inside the dispatch_users function, locust won't gracefully shutdown. self.quit() - logger.info( - "All users spawned: %s (%i total running)" - % ( - ", ".join("%s: %d" % (name, count) for name, count in self.user_classes_count.items()), - sum(self.user_classes_count.values()), - ) - ) + logger.info("All users spawned: %s" % _format_user_classes_count_for_log(self.user_classes_count)) self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_classes_count.values())) @@ -660,15 +645,22 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: self.spawn_rate = spawn_rate - worker_spawn_rate = float(spawn_rate) / (num_workers or 1) logger.info( - "Sending spawn jobs of %d users and %.2f spawn rate to %d ready clients" - % (user_count, worker_spawn_rate, num_workers) + "Sending spawn jobs of %d users at %.2f spawn rate to %d ready clients" + % (user_count, spawn_rate, num_workers) ) - if worker_spawn_rate > 100: + # Prior to the refactoring from https://github.com/locustio/locust/pull/1621, this warning + # was logged if `spawn_rate / number_of_workers` was above 100. However, the master + # is now responsible for dispatching and controlling the spawn rate which is more CPU intensive for + # the master. The number 100 is a little arbitrary as the computational load on the master greatly + # depends on the number of workers and the number of user classes. For instance, 5 user classes and 5 workers + # can easily do 200/s. However, 200/s with 50 workers and 20 user classes will likely make the dispatch very + # slow because of the required computations. I (@mboutet) doubt that many Locust's users are spawning + # that rapidly. If so, then they'll likely open issues on GitHub in which case I'll (@mboutet) take a look. + if spawn_rate > 100: logger.warning( - "Your selected spawn rate is very high (>100/worker), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" + "Your selected spawn rate is high (>100), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" ) if self.state != STATE_RUNNING and self.state != STATE_SPAWNING: @@ -701,15 +693,16 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: Message("spawn", data, worker_node_id), ) ) - logger.debug("Sending spawn message to %i client(s)" % len(dispatch_greenlets)) + dispatched_user_count = sum(map(sum, map(methodcaller("values"), dispatched_users.values()))) + logger.debug( + "Sending spawn messages for %g total users to %i client(s)", + dispatched_user_count, + len(dispatch_greenlets), + ) dispatch_greenlets.join() logger.debug( - "Currently spawned users: %s (%i total running)" - % ( - ", ".join("%s: %d" % (name, count) for name, count in self.reported_user_classes_count.items()), - sum(self.reported_user_classes_count.values()), - ) + "Currently spawned users: %s" % _format_user_classes_count_for_log(self.reported_user_classes_count) ) except KeyboardInterrupt: @@ -732,13 +725,7 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_classes_count.values())) - logger.info( - "All users spawned: %s (%i total running)" - % ( - ", ".join("%s: %d" % (name, count) for name, count in self.reported_user_classes_count.items()), - sum(self.reported_user_classes_count.values()), - ) - ) + logger.info("All users spawned: %s" % _format_user_classes_count_for_log(self.reported_user_classes_count)) @functools.lru_cache() def _wait_for_workers_report_after_ramp_up(self) -> float: @@ -1135,3 +1122,10 @@ def _send_stats(self): data = {} self.environment.events.report_to_master.fire(client_id=self.client_id, data=data) self.client.send(Message("stats", data, self.client_id)) + + +def _format_user_classes_count_for_log(user_classes_count: Dict[str, int]) -> str: + return "{} ({} total users)".format( + json.dumps(dict(sorted(user_classes_count.items(), key=itemgetter(0)))), + sum(user_classes_count.values()), + ) diff --git a/locust/test/test_main.py b/locust/test/test_main.py index eecc3a56cd..ca0a41db36 100644 --- a/locust/test/test_main.py +++ b/locust/test/test_main.py @@ -381,14 +381,14 @@ def t(self): output = proc.communicate()[0].decode("utf-8") stdin.close() self.assertIn('Spawning additional {"UserSubclass": 1} ({"UserSubclass": 0} already running)...', output) - self.assertIn("0 Users have been stopped, 1 still running", output) + self.assertIn("0 users have been stopped, 1 still running", output) self.assertIn('Spawning additional {"UserSubclass": 10} ({"UserSubclass": 1} already running)...', output) self.assertIn('Spawning additional {} ({"UserSubclass": 11} already running)...', output) - self.assertIn("1 Users have been stopped, 10 still running", output) + self.assertIn("1 users have been stopped, 10 still running", output) self.assertIn('Spawning additional {} ({"UserSubclass": 10} already running)...', output) - self.assertIn("10 Users have been stopped, 0 still running", output) + self.assertIn("10 users have been stopped, 0 still running", output) self.assertIn('Spawning additional {} ({"UserSubclass": 0} already running)...', output) - self.assertIn("10 Users have been stopped, 0 still running", output) + self.assertIn("10 users have been stopped, 0 still running", output) self.assertIn("Test task is running", output) self.assertIn("Shutting down (exit code 0), bye.", output) self.assertEqual(0, proc.returncode) From d7494851f3ccdfea427dc21d4ad2733fcc5d4144 Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 4 Jul 2021 13:12:23 -0400 Subject: [PATCH 136/139] Reinstate the previous warning regarding the workers' spawn rate --- locust/runners.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index f58e00f292..2a81b4dfa2 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -650,17 +650,21 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: % (user_count, spawn_rate, num_workers) ) - # Prior to the refactoring from https://github.com/locustio/locust/pull/1621, this warning - # was logged if `spawn_rate / number_of_workers` was above 100. However, the master - # is now responsible for dispatching and controlling the spawn rate which is more CPU intensive for - # the master. The number 100 is a little arbitrary as the computational load on the master greatly - # depends on the number of workers and the number of user classes. For instance, 5 user classes and 5 workers - # can easily do 200/s. However, 200/s with 50 workers and 20 user classes will likely make the dispatch very - # slow because of the required computations. I (@mboutet) doubt that many Locust's users are spawning - # that rapidly. If so, then they'll likely open issues on GitHub in which case I'll (@mboutet) take a look. + worker_spawn_rate = float(spawn_rate) / (num_workers or 1) + if worker_spawn_rate > 100: + logger.warning( + "Your selected spawn rate is very high (>100/worker), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" + ) + + # Since https://github.com/locustio/locust/pull/1621, the master is responsible for dispatching and controlling + # the total spawn rate which is more CPU intensive for the master. The number 100 is a little arbitrary as the computational + # load on the master greatly depends on the number of workers and the number of user classes. For instance, + # 5 user classes and 5 workers can easily do 200/s. However, 200/s with 50 workers and 20 user classes will likely make the + # dispatch very slow because of the required computations. I (@mboutet) doubt that many Locust's users are + # spawning that rapidly. If so, then they'll likely open issues on GitHub in which case I'll (@mboutet) take a look. if spawn_rate > 100: logger.warning( - "Your selected spawn rate is high (>100), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" + "Your selected total spawn rate is high (>100), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" ) if self.state != STATE_RUNNING and self.state != STATE_SPAWNING: From 11a7825b33682eff35925f6d59b31164bda8654c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 4 Jul 2021 13:12:51 -0400 Subject: [PATCH 137/139] Use `Ramping to %d users using a %.2f spawn rate` as the log msg --- locust/runners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 2a81b4dfa2..0b35ad09d6 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -306,7 +306,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): if self.state != STATE_INIT and self.state != STATE_STOPPED: self.update_state(STATE_SPAWNING) - logger.info("Updating test with %d users, %.2f spawn rate and wait=%r" % (user_count, spawn_rate, wait)) + logger.info("Ramping to %d users using a %.2f spawn rate" % (user_count, spawn_rate)) try: for dispatched_users in UsersDispatcher( @@ -317,7 +317,7 @@ def start(self, user_count: int, spawn_rate: float, wait: bool = False): user_classes_spawn_count = {} user_classes_stop_count = {} user_classes_count = dispatched_users[local_worker_node.id] - logger.debug("Updating running test with %s" % _format_user_classes_count_for_log(user_classes_count)) + logger.debug("Ramping to %s" % _format_user_classes_count_for_log(user_classes_count)) for user_class, user_class_count in user_classes_count.items(): if self.user_classes_count[user_class] > user_class_count: user_classes_stop_count[user_class] = self.user_classes_count[user_class] - user_class_count From c9eb174971c86fb07e00b7fa12e476463d767c9c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 4 Jul 2021 15:16:00 -0400 Subject: [PATCH 138/139] Increase warning threshold for total spawn rate and reword the warning content. --- locust/runners.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locust/runners.py b/locust/runners.py index 0b35ad09d6..8e107ce4fc 100644 --- a/locust/runners.py +++ b/locust/runners.py @@ -657,14 +657,15 @@ def start(self, user_count: int, spawn_rate: float, **kwargs) -> None: ) # Since https://github.com/locustio/locust/pull/1621, the master is responsible for dispatching and controlling - # the total spawn rate which is more CPU intensive for the master. The number 100 is a little arbitrary as the computational + # the total spawn rate which is more CPU intensive for the master. The number 200 is a little arbitrary as the computational # load on the master greatly depends on the number of workers and the number of user classes. For instance, # 5 user classes and 5 workers can easily do 200/s. However, 200/s with 50 workers and 20 user classes will likely make the # dispatch very slow because of the required computations. I (@mboutet) doubt that many Locust's users are # spawning that rapidly. If so, then they'll likely open issues on GitHub in which case I'll (@mboutet) take a look. - if spawn_rate > 100: + if spawn_rate > 200: logger.warning( - "Your selected total spawn rate is high (>100), and this is known to sometimes cause issues. Do you really need to ramp up that fast?" + "Your selected total spawn rate is quite high (>200), and this is known to sometimes cause performance issues on the master. " + "Do you really need to ramp up that fast? If so and if encountering performance issues on the master, free to open an issue." ) if self.state != STATE_RUNNING and self.state != STATE_SPAWNING: From 7d67e239b2b7c478098ae66f372a4fe91de2990c Mon Sep 17 00:00:00 2001 From: Maxence Boutet Date: Sun, 4 Jul 2021 15:19:13 -0400 Subject: [PATCH 139/139] Remove unused import in html.py --- locust/html.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/locust/html.py b/locust/html.py index 3df99c4a41..49f4ebb97e 100644 --- a/locust/html.py +++ b/locust/html.py @@ -1,5 +1,3 @@ -from copy import deepcopy - from jinja2 import Environment, FileSystemLoader import os import pathlib