Skip to content

Commit 18b19e0

Browse files
authored
Merge pull request #2650 from samuelspagl/fix/run-custom-messages-in-greenlet
Add functionality to run listener functions for `custom_messages` concurrently
2 parents af8c067 + 78dc88e commit 18b19e0

File tree

4 files changed

+65
-7
lines changed

4 files changed

+65
-7
lines changed

Diff for: docs/running-distributed.rst

+14-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,20 @@ order to coordinate the test. This can be easily accomplished with custom messag
131131
environment.runner.send_message('test_users', users)
132132
133133
Note that when running locally (i.e. non-distributed), this functionality will be preserved;
134-
the messages will simply be handled by the runner that sends them.
134+
the messages will simply be handled by the runner that sends them.
135+
136+
.. note::
137+
Using the default options while registering a message handler will run the listener function
138+
in a **blocking** way, resulting in the heartbeat and other messages being delayed for the amount
139+
of the execution.
140+
If it is known that the listener function will handle time-intensive tasks, it is possible to register the
141+
function as **concurrent** (as a separate greenlet).
142+
143+
.. code-block::
144+
environment.runner.register_message('test_users', setup_test_users, concurrent=True)
145+
146+
Please use this feature with care, as otherwise it could result in greenlets running and influencing
147+
the running loadtest.
135148

136149
For more details, see the `complete example <https://github.com/locustio/locust/tree/master/examples/custom_messages.py>`_.
137150

Diff for: examples/custom_messages.py

+12
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from locust import HttpUser, between, events, task
22
from locust.runners import MasterRunner, WorkerRunner
33

4+
import gevent
5+
46
usernames = []
57

68

79
def setup_test_users(environment, msg, **kwargs):
810
# Fired when the worker receives a message of type 'test_users'
911
usernames.extend(map(lambda u: u["name"], msg.data))
12+
# Even though "acknowledge_concurrent_users" was sent first, "acknowledge_users"
13+
# will print its statement first, as "acknowledge_concurrent_users" was registered
14+
# running concurrently, and therefore not blocking other messages.
15+
environment.runner.send_message("concurrent_message", "This is a non blocking message")
1016
environment.runner.send_message("acknowledge_users", f"Thanks for the {len(msg.data)} users!")
1117

1218

@@ -15,12 +21,18 @@ def on_acknowledge(msg, **kwargs):
1521
print(msg.data)
1622

1723

24+
def on_concurrent_message(msg, **kwargs):
25+
gevent.sleep(10)
26+
print(msg.data)
27+
28+
1829
@events.init.add_listener
1930
def on_locust_init(environment, **_kwargs):
2031
if not isinstance(environment.runner, MasterRunner):
2132
environment.runner.register_message("test_users", setup_test_users)
2233
if not isinstance(environment.runner, WorkerRunner):
2334
environment.runner.register_message("acknowledge_users", on_acknowledge)
35+
environment.runner.register_message("concurrent_message", on_concurrent_message, concurrent=True)
2436

2537

2638
@events.test_start.add_listener

Diff for: locust/runners.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def __init__(self, environment: Environment) -> None:
119119
self.target_user_classes_count: dict[str, int] = {}
120120
# target_user_count is set before the ramp-up/ramp-down occurs.
121121
self.target_user_count: int = 0
122-
self.custom_messages: dict[str, Callable] = {}
122+
self.custom_messages: dict[str, tuple[Callable, bool]] = {}
123123

124124
self._users_dispatcher: UsersDispatcher | None = None
125125

@@ -420,7 +420,7 @@ def log_exception(self, node_id: str, msg: str, formatted_tb: str) -> None:
420420
row["nodes"].add(node_id)
421421
self.exceptions[key] = row
422422

423-
def register_message(self, msg_type: str, listener: Callable) -> None:
423+
def register_message(self, msg_type: str, listener: Callable, concurrent=False) -> None:
424424
"""
425425
Register a listener for a custom message from another node
426426
@@ -429,7 +429,7 @@ def register_message(self, msg_type: str, listener: Callable) -> None:
429429
"""
430430
if msg_type in self.custom_messages:
431431
raise Exception(f"Tried to register listener method for {msg_type}, but it already had a listener!")
432-
self.custom_messages[msg_type] = listener
432+
self.custom_messages[msg_type] = (listener, concurrent)
433433

434434

435435
class LocalRunner(Runner):
@@ -568,7 +568,7 @@ def send_message(self, msg_type: str, data: Any | None = None, client_id: str |
568568
"""
569569
logger.debug("Running locally: sending %s message to self" % msg_type)
570570
if msg_type in self.custom_messages:
571-
listener = self.custom_messages[msg_type]
571+
listener, concurrent = self.custom_messages[msg_type]
572572
msg = Message(msg_type, data, "local")
573573
listener(environment=self.environment, msg=msg)
574574
else:
@@ -1139,7 +1139,11 @@ def client_listener(self) -> NoReturn:
11391139
f"Received {msg.type} message from worker {msg.node_id} (index {self.get_worker_index(msg.node_id)})"
11401140
)
11411141
try:
1142-
self.custom_messages[msg.type](environment=self.environment, msg=msg)
1142+
listener, concurrent = self.custom_messages[msg.type]
1143+
if not concurrent:
1144+
listener(environment=self.environment, msg=msg)
1145+
else:
1146+
gevent.spawn(listener, self.environment, msg)
11431147
except Exception:
11441148
logging.error(f"Uncaught exception in handler for {msg.type}\n{traceback.format_exc()}")
11451149

@@ -1393,7 +1397,11 @@ def worker(self) -> NoReturn:
13931397
self.last_heartbeat_timestamp = time.time()
13941398
elif msg.type in self.custom_messages:
13951399
logger.debug("Received %s message from master" % msg.type)
1396-
self.custom_messages[msg.type](environment=self.environment, msg=msg)
1400+
listener, concurrent = self.custom_messages[msg.type]
1401+
if not concurrent:
1402+
listener(environment=self.environment, msg=msg)
1403+
else:
1404+
gevent.spawn(listener, self.environment, msg)
13971405
else:
13981406
logger.warning(f"Unknown message type received: {msg.type}")
13991407

Diff for: locust/test/test_runners.py

+25
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,31 @@ def on_custom_msg(msg, **kw):
606606
self.assertTrue(test_custom_msg[0])
607607
self.assertEqual(123, test_custom_msg_data[0]["test_data"])
608608

609+
def test_concurrent_custom_message(self):
610+
class MyUser(User):
611+
wait_time = constant(1)
612+
613+
@task
614+
def my_task(self):
615+
pass
616+
617+
test_custom_msg = [False]
618+
test_custom_msg_data = [{}]
619+
620+
def on_custom_msg(msg, **kw):
621+
test_custom_msg[0] = True
622+
test_custom_msg_data[0] = msg.data
623+
624+
environment = Environment(user_classes=[MyUser])
625+
runner = LocalRunner(environment)
626+
627+
runner.register_message("test_custom_msg", on_custom_msg, concurrent=True)
628+
runner.send_message("test_custom_msg", {"test_data": 123})
629+
630+
gevent.sleep(0.5)
631+
self.assertTrue(test_custom_msg[0])
632+
self.assertEqual(123, test_custom_msg_data[0]["test_data"])
633+
609634
def test_undefined_custom_message(self):
610635
class MyUser(User):
611636
wait_time = constant(1)

0 commit comments

Comments
 (0)