locust 2.20.1.dev26__py3-none-any.whl → 2.20.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- locust/__init__.py +7 -7
- locust/_version.py +2 -2
- locust/argument_parser.py +35 -18
- locust/clients.py +7 -6
- locust/contrib/fasthttp.py +42 -37
- locust/debug.py +14 -8
- locust/dispatch.py +25 -25
- locust/env.py +45 -37
- locust/event.py +13 -10
- locust/exception.py +0 -9
- locust/html.py +9 -8
- locust/input_events.py +7 -5
- locust/main.py +95 -47
- locust/rpc/protocol.py +4 -2
- locust/rpc/zmqrpc.py +6 -4
- locust/runners.py +69 -76
- locust/shape.py +7 -4
- locust/stats.py +73 -71
- locust/templates/index.html +8 -23
- locust/test/mock_logging.py +7 -5
- locust/test/test_debugging.py +4 -4
- locust/test/test_dispatch.py +10 -9
- locust/test/test_env.py +30 -1
- locust/test/test_fasthttp.py +9 -8
- locust/test/test_http.py +4 -3
- locust/test/test_interruptable_task.py +4 -4
- locust/test/test_load_locustfile.py +6 -5
- locust/test/test_locust_class.py +11 -5
- locust/test/test_log.py +5 -4
- locust/test/test_main.py +37 -7
- locust/test/test_parser.py +11 -15
- locust/test/test_runners.py +22 -21
- locust/test/test_sequential_taskset.py +3 -2
- locust/test/test_stats.py +16 -18
- locust/test/test_tags.py +3 -2
- locust/test/test_taskratio.py +3 -3
- locust/test/test_users.py +3 -3
- locust/test/test_util.py +3 -2
- locust/test/test_wait_time.py +3 -3
- locust/test/test_web.py +142 -27
- locust/test/test_zmqrpc.py +5 -3
- locust/test/testcases.py +7 -7
- locust/test/util.py +6 -7
- locust/user/__init__.py +1 -1
- locust/user/inspectuser.py +6 -5
- locust/user/sequential_taskset.py +3 -1
- locust/user/task.py +22 -27
- locust/user/users.py +17 -7
- locust/util/deprecation.py +0 -1
- locust/util/exception_handler.py +1 -1
- locust/util/load_locustfile.py +4 -2
- locust/web.py +102 -53
- locust/webui/dist/assets/auth-5e21717c.js.map +1 -0
- locust/webui/dist/assets/{index-01afe4fa.js → index-a83a5dd9.js} +85 -84
- locust/webui/dist/assets/index-a83a5dd9.js.map +1 -0
- locust/webui/dist/auth.html +20 -0
- locust/webui/dist/index.html +1 -1
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
- locust-2.20.2.dist-info/RECORD +105 -0
- locust-2.20.1.dev26.dist-info/RECORD +0 -102
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
- {locust-2.20.1.dev26.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/runners.py
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from locust import __version__
|
4
|
+
|
1
5
|
import functools
|
6
|
+
import inspect
|
2
7
|
import json
|
3
8
|
import logging
|
4
9
|
import os
|
@@ -17,30 +22,23 @@ from operator import (
|
|
17
22
|
from types import TracebackType
|
18
23
|
from typing import (
|
19
24
|
TYPE_CHECKING,
|
20
|
-
|
25
|
+
Any,
|
26
|
+
Callable,
|
21
27
|
Iterator,
|
22
|
-
List,
|
23
28
|
NoReturn,
|
29
|
+
TypedDict,
|
24
30
|
ValuesView,
|
25
|
-
Set,
|
26
|
-
Optional,
|
27
|
-
Tuple,
|
28
|
-
Type,
|
29
|
-
Any,
|
30
31
|
cast,
|
31
|
-
Callable,
|
32
|
-
TypedDict,
|
33
32
|
)
|
34
33
|
from uuid import uuid4
|
34
|
+
|
35
35
|
import gevent
|
36
36
|
import greenlet
|
37
37
|
import psutil
|
38
38
|
from gevent.event import Event
|
39
39
|
from gevent.pool import Group
|
40
|
-
import inspect
|
41
40
|
|
42
|
-
from . import
|
43
|
-
from locust import __version__
|
41
|
+
from . import argument_parser
|
44
42
|
from .dispatch import UsersDispatcher
|
45
43
|
from .exception import RPCError, RPCReceiveError, RPCSendError
|
46
44
|
from .log import greenlet_exception_logger
|
@@ -53,9 +51,9 @@ from .stats import (
|
|
53
51
|
StatsError,
|
54
52
|
setup_distributed_stats_event_listeners,
|
55
53
|
)
|
56
|
-
from . import argument_parser
|
57
54
|
|
58
55
|
if TYPE_CHECKING:
|
56
|
+
from . import User
|
59
57
|
from .env import Environment
|
60
58
|
|
61
59
|
logger = logging.getLogger(__name__)
|
@@ -87,7 +85,7 @@ class ExceptionDict(TypedDict):
|
|
87
85
|
count: int
|
88
86
|
msg: str
|
89
87
|
traceback: str
|
90
|
-
nodes:
|
88
|
+
nodes: set[str]
|
91
89
|
|
92
90
|
|
93
91
|
class Runner:
|
@@ -101,29 +99,29 @@ class Runner:
|
|
101
99
|
desired type.
|
102
100
|
"""
|
103
101
|
|
104
|
-
def __init__(self, environment:
|
102
|
+
def __init__(self, environment: Environment) -> None:
|
105
103
|
self.environment = environment
|
106
104
|
self.user_greenlets = Group()
|
107
105
|
self.greenlet = Group()
|
108
106
|
self.state = STATE_INIT
|
109
|
-
self.spawning_greenlet:
|
110
|
-
self.shape_greenlet:
|
111
|
-
self.shape_last_tick:
|
107
|
+
self.spawning_greenlet: gevent.Greenlet | None = None
|
108
|
+
self.shape_greenlet: gevent.Greenlet | None = None
|
109
|
+
self.shape_last_tick: tuple[int, float] | tuple[int, float, list[type[User]] | None] | None = None
|
112
110
|
self.current_cpu_usage: int = 0
|
113
111
|
self.cpu_warning_emitted: bool = False
|
114
112
|
self.worker_cpu_warning_emitted: bool = False
|
115
113
|
self.current_memory_usage: int = 0
|
116
114
|
self.greenlet.spawn(self.monitor_cpu_and_memory).link_exception(greenlet_exception_handler)
|
117
|
-
self.exceptions:
|
115
|
+
self.exceptions: dict[int, ExceptionDict] = {}
|
118
116
|
# Because of the way the ramp-up/ramp-down is implemented, target_user_classes_count
|
119
117
|
# is only updated at the end of the ramp-up/ramp-down.
|
120
118
|
# See https://github.com/locustio/locust/issues/1883#issuecomment-919239824 for context.
|
121
|
-
self.target_user_classes_count:
|
119
|
+
self.target_user_classes_count: dict[str, int] = {}
|
122
120
|
# target_user_count is set before the ramp-up/ramp-down occurs.
|
123
121
|
self.target_user_count: int = 0
|
124
|
-
self.custom_messages:
|
122
|
+
self.custom_messages: dict[str, Callable] = {}
|
125
123
|
|
126
|
-
self._users_dispatcher:
|
124
|
+
self._users_dispatcher: UsersDispatcher | None = None
|
127
125
|
|
128
126
|
# set up event listeners for recording requests
|
129
127
|
def on_request(request_type, name, response_time, response_length, exception=None, **_kwargs):
|
@@ -134,7 +132,7 @@ class Runner:
|
|
134
132
|
self.environment.events.request.add_listener(on_request)
|
135
133
|
|
136
134
|
self.connection_broken = False
|
137
|
-
self.final_user_classes_count:
|
135
|
+
self.final_user_classes_count: dict[str, int] = {} # just for the ratio report, fills before runner stops
|
138
136
|
|
139
137
|
# register listener that resets stats when spawning is complete
|
140
138
|
def on_spawning_complete(user_count: int) -> None:
|
@@ -151,11 +149,11 @@ class Runner:
|
|
151
149
|
self.greenlet.kill(block=False)
|
152
150
|
|
153
151
|
@property
|
154
|
-
def user_classes(self) ->
|
152
|
+
def user_classes(self) -> list[type[User]]:
|
155
153
|
return self.environment.user_classes
|
156
154
|
|
157
155
|
@property
|
158
|
-
def user_classes_by_name(self) ->
|
156
|
+
def user_classes_by_name(self) -> dict[str, type[User]]:
|
159
157
|
return self.environment.user_classes_by_name
|
160
158
|
|
161
159
|
@property
|
@@ -163,7 +161,7 @@ class Runner:
|
|
163
161
|
return self.environment.stats
|
164
162
|
|
165
163
|
@property
|
166
|
-
def errors(self) ->
|
164
|
+
def errors(self) -> dict[str, StatsError]:
|
167
165
|
return self.stats.errors
|
168
166
|
|
169
167
|
@property
|
@@ -174,7 +172,7 @@ class Runner:
|
|
174
172
|
return len(self.user_greenlets)
|
175
173
|
|
176
174
|
@property
|
177
|
-
def user_classes_count(self) ->
|
175
|
+
def user_classes_count(self) -> dict[str, int]:
|
178
176
|
"""
|
179
177
|
:returns: Number of currently running users for each user class
|
180
178
|
"""
|
@@ -214,18 +212,17 @@ class Runner:
|
|
214
212
|
)
|
215
213
|
return self.cpu_warning_emitted
|
216
214
|
|
217
|
-
def spawn_users(self, user_classes_spawn_count:
|
215
|
+
def spawn_users(self, user_classes_spawn_count: dict[str, int], wait: bool = False):
|
218
216
|
if self.state == STATE_INIT or self.state == STATE_STOPPED:
|
219
217
|
self.update_state(STATE_SPAWNING)
|
220
218
|
|
221
219
|
logger.debug(
|
222
|
-
"Spawning additional
|
223
|
-
% (json.dumps(user_classes_spawn_count), json.dumps(self.user_classes_count))
|
220
|
+
f"Spawning additional {json.dumps(user_classes_spawn_count)} ({json.dumps(self.user_classes_count)} already running)..."
|
224
221
|
)
|
225
222
|
|
226
|
-
def spawn(user_class: str, spawn_count: int) ->
|
223
|
+
def spawn(user_class: str, spawn_count: int) -> list[User]:
|
227
224
|
n = 0
|
228
|
-
new_users:
|
225
|
+
new_users: list[User] = []
|
229
226
|
while n < spawn_count:
|
230
227
|
new_user = self.user_classes_by_name[user_class](self.environment)
|
231
228
|
new_user.start(self.user_greenlets)
|
@@ -236,7 +233,7 @@ class Runner:
|
|
236
233
|
logger.debug("All users of class %s spawned" % user_class)
|
237
234
|
return new_users
|
238
235
|
|
239
|
-
new_users:
|
236
|
+
new_users: list[User] = []
|
240
237
|
for user_class, spawn_count in user_classes_spawn_count.items():
|
241
238
|
new_users += spawn(user_class, spawn_count)
|
242
239
|
|
@@ -245,7 +242,7 @@ class Runner:
|
|
245
242
|
logger.info("All users stopped\n")
|
246
243
|
return new_users
|
247
244
|
|
248
|
-
def stop_users(self, user_classes_stop_count:
|
245
|
+
def stop_users(self, user_classes_stop_count: dict[str, int]) -> None:
|
249
246
|
async_calls_to_stop = Group()
|
250
247
|
stop_group = Group()
|
251
248
|
|
@@ -253,7 +250,7 @@ class Runner:
|
|
253
250
|
if self.user_classes_count[user_class] == 0:
|
254
251
|
continue
|
255
252
|
|
256
|
-
to_stop:
|
253
|
+
to_stop: list[greenlet.greenlet] = []
|
257
254
|
for user_greenlet in self.user_greenlets:
|
258
255
|
if len(to_stop) == stop_count:
|
259
256
|
break
|
@@ -313,12 +310,12 @@ class Runner:
|
|
313
310
|
|
314
311
|
@abstractmethod
|
315
312
|
def start(
|
316
|
-
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes:
|
313
|
+
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list[type[User]] | None = None
|
317
314
|
) -> None:
|
318
315
|
...
|
319
316
|
|
320
317
|
@abstractmethod
|
321
|
-
def send_message(self, msg_type: str, data:
|
318
|
+
def send_message(self, msg_type: str, data: Any | None = None, client_id: str | None = None) -> None:
|
322
319
|
...
|
323
320
|
|
324
321
|
def start_shape(self) -> None:
|
@@ -462,9 +459,7 @@ class LocalRunner(Runner):
|
|
462
459
|
|
463
460
|
self.environment.events.user_error.add_listener(on_user_error)
|
464
461
|
|
465
|
-
def _start(
|
466
|
-
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: Optional[list] = None
|
467
|
-
) -> None:
|
462
|
+
def _start(self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list | None = None) -> None:
|
468
463
|
"""
|
469
464
|
Start running a load test
|
470
465
|
|
@@ -507,8 +502,8 @@ class LocalRunner(Runner):
|
|
507
502
|
|
508
503
|
try:
|
509
504
|
for dispatched_users in self._users_dispatcher:
|
510
|
-
user_classes_spawn_count:
|
511
|
-
user_classes_stop_count:
|
505
|
+
user_classes_spawn_count: dict[str, int] = {}
|
506
|
+
user_classes_stop_count: dict[str, int] = {}
|
512
507
|
user_classes_count = dispatched_users[self._local_worker_node.id]
|
513
508
|
logger.debug("Ramping to %s" % _format_user_classes_count_for_log(user_classes_count))
|
514
509
|
for user_class_name, user_class_count in user_classes_count.items():
|
@@ -546,7 +541,7 @@ class LocalRunner(Runner):
|
|
546
541
|
self.environment.events.spawning_complete.fire(user_count=sum(self.target_user_classes_count.values()))
|
547
542
|
|
548
543
|
def start(
|
549
|
-
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes:
|
544
|
+
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list[type[User]] | None = None
|
550
545
|
) -> None:
|
551
546
|
if spawn_rate > 100:
|
552
547
|
logger.warning(
|
@@ -566,7 +561,7 @@ class LocalRunner(Runner):
|
|
566
561
|
return
|
567
562
|
super().stop()
|
568
563
|
|
569
|
-
def send_message(self, msg_type: str, data:
|
564
|
+
def send_message(self, msg_type: str, data: Any | None = None, client_id: str | None = None) -> None:
|
570
565
|
"""
|
571
566
|
Emulates internodal messaging by calling registered listeners
|
572
567
|
|
@@ -597,7 +592,7 @@ class WorkerNode:
|
|
597
592
|
self.cpu_warning_emitted = False
|
598
593
|
self.memory_usage: int = 0
|
599
594
|
# The reported users running on the worker
|
600
|
-
self.user_classes_count:
|
595
|
+
self.user_classes_count: dict[str, int] = {}
|
601
596
|
|
602
597
|
@property
|
603
598
|
def user_count(self) -> int:
|
@@ -606,9 +601,9 @@ class WorkerNode:
|
|
606
601
|
|
607
602
|
class WorkerNodes(MutableMapping):
|
608
603
|
def __init__(self):
|
609
|
-
self._worker_nodes:
|
604
|
+
self._worker_nodes: dict[str, WorkerNode] = {}
|
610
605
|
|
611
|
-
def get_by_state(self, state) ->
|
606
|
+
def get_by_state(self, state) -> list[WorkerNode]:
|
612
607
|
return [c for c in self.values() if c.state == state]
|
613
608
|
|
614
609
|
@property
|
@@ -616,19 +611,19 @@ class WorkerNodes(MutableMapping):
|
|
616
611
|
return self.values()
|
617
612
|
|
618
613
|
@property
|
619
|
-
def ready(self) ->
|
614
|
+
def ready(self) -> list[WorkerNode]:
|
620
615
|
return self.get_by_state(STATE_INIT)
|
621
616
|
|
622
617
|
@property
|
623
|
-
def spawning(self) ->
|
618
|
+
def spawning(self) -> list[WorkerNode]:
|
624
619
|
return self.get_by_state(STATE_SPAWNING)
|
625
620
|
|
626
621
|
@property
|
627
|
-
def running(self) ->
|
622
|
+
def running(self) -> list[WorkerNode]:
|
628
623
|
return self.get_by_state(STATE_RUNNING)
|
629
624
|
|
630
625
|
@property
|
631
|
-
def missing(self) ->
|
626
|
+
def missing(self) -> list[WorkerNode]:
|
632
627
|
return self.get_by_state(STATE_MISSING)
|
633
628
|
|
634
629
|
def __setitem__(self, k: str, v: WorkerNode) -> None:
|
@@ -687,13 +682,13 @@ class MasterRunner(DistributedRunner):
|
|
687
682
|
else:
|
688
683
|
raise
|
689
684
|
|
690
|
-
self._users_dispatcher:
|
685
|
+
self._users_dispatcher: UsersDispatcher | None = None
|
691
686
|
|
692
687
|
self.greenlet.spawn(self.heartbeat_worker).link_exception(greenlet_exception_handler)
|
693
688
|
self.greenlet.spawn(self.client_listener).link_exception(greenlet_exception_handler)
|
694
689
|
|
695
690
|
# listener that gathers info on how many users the worker has spawned
|
696
|
-
def on_worker_report(client_id: str, data:
|
691
|
+
def on_worker_report(client_id: str, data: dict[str, Any]) -> None:
|
697
692
|
if client_id not in self.clients:
|
698
693
|
logger.info("Discarded report from unrecognized worker %s", client_id)
|
699
694
|
return
|
@@ -702,7 +697,7 @@ class MasterRunner(DistributedRunner):
|
|
702
697
|
self.environment.events.worker_report.add_listener(on_worker_report)
|
703
698
|
|
704
699
|
# register listener that sends quit message to worker nodes
|
705
|
-
def on_quitting(environment:
|
700
|
+
def on_quitting(environment: Environment, **kw):
|
706
701
|
self.quit()
|
707
702
|
|
708
703
|
self.environment.events.quitting.add_listener(on_quitting)
|
@@ -737,7 +732,7 @@ class MasterRunner(DistributedRunner):
|
|
737
732
|
return warning_emitted
|
738
733
|
|
739
734
|
def start(
|
740
|
-
self, user_count: int, spawn_rate: float, wait=False, user_classes:
|
735
|
+
self, user_count: int, spawn_rate: float, wait=False, user_classes: list[type[User]] | None = None
|
741
736
|
) -> None:
|
742
737
|
self.spawning_completed = False
|
743
738
|
|
@@ -908,7 +903,7 @@ class MasterRunner(DistributedRunner):
|
|
908
903
|
self.stop(send_stop_to_client=False)
|
909
904
|
logger.debug("Quitting...")
|
910
905
|
for client in self.clients.all:
|
911
|
-
logger.debug("Sending quit message to worker
|
906
|
+
logger.debug(f"Sending quit message to worker {client.id} (index {self.get_worker_index(client.id)})")
|
912
907
|
self.server.send_to_client(Message("quit", None, client.id))
|
913
908
|
gevent.sleep(0.5) # wait for final stats report from all workers
|
914
909
|
self.greenlet.kill(block=True)
|
@@ -1128,14 +1123,14 @@ class MasterRunner(DistributedRunner):
|
|
1128
1123
|
return len(self.clients.ready) + len(self.clients.spawning) + len(self.clients.running)
|
1129
1124
|
|
1130
1125
|
@property
|
1131
|
-
def reported_user_classes_count(self) ->
|
1132
|
-
reported_user_classes_count:
|
1126
|
+
def reported_user_classes_count(self) -> dict[str, int]:
|
1127
|
+
reported_user_classes_count: dict[str, int] = defaultdict(int)
|
1133
1128
|
for client in self.clients.ready + self.clients.spawning + self.clients.running:
|
1134
1129
|
for name, count in client.user_classes_count.items():
|
1135
1130
|
reported_user_classes_count[name] += count
|
1136
1131
|
return reported_user_classes_count
|
1137
1132
|
|
1138
|
-
def send_message(self, msg_type: str, data:
|
1133
|
+
def send_message(self, msg_type: str, data: dict[str, Any] | None = None, client_id: str | None = None):
|
1139
1134
|
"""
|
1140
1135
|
Sends a message to attached worker node(s)
|
1141
1136
|
|
@@ -1145,11 +1140,11 @@ class MasterRunner(DistributedRunner):
|
|
1145
1140
|
If None, will send to all attached workers
|
1146
1141
|
"""
|
1147
1142
|
if client_id:
|
1148
|
-
logger.debug("Sending
|
1143
|
+
logger.debug(f"Sending {msg_type} message to worker {client_id}")
|
1149
1144
|
self.server.send_to_client(Message(msg_type, data, client_id))
|
1150
1145
|
else:
|
1151
1146
|
for client in self.clients.all:
|
1152
|
-
logger.debug("Sending
|
1147
|
+
logger.debug(f"Sending {msg_type} message to worker {client.id}")
|
1153
1148
|
self.server.send_to_client(Message(msg_type, data, client.id))
|
1154
1149
|
|
1155
1150
|
|
@@ -1165,7 +1160,7 @@ class WorkerRunner(DistributedRunner):
|
|
1165
1160
|
# the worker index is set on ACK, if master provided it (masters <= 2.10.2 do not provide it)
|
1166
1161
|
worker_index = -1
|
1167
1162
|
|
1168
|
-
def __init__(self, environment:
|
1163
|
+
def __init__(self, environment: Environment, master_host: str, master_port: int) -> None:
|
1169
1164
|
"""
|
1170
1165
|
:param environment: Environment instance
|
1171
1166
|
:param master_host: Host/IP to use for connection to the master
|
@@ -1174,14 +1169,14 @@ class WorkerRunner(DistributedRunner):
|
|
1174
1169
|
super().__init__(environment)
|
1175
1170
|
self.retry = 0
|
1176
1171
|
self.connected = False
|
1177
|
-
self.last_heartbeat_timestamp:
|
1172
|
+
self.last_heartbeat_timestamp: float | None = None
|
1178
1173
|
self.connection_event = Event()
|
1179
1174
|
self.worker_state = STATE_INIT
|
1180
1175
|
self.client_id = socket.gethostname() + "_" + uuid4().hex
|
1181
1176
|
self.master_host = master_host
|
1182
1177
|
self.master_port = master_port
|
1183
1178
|
self.worker_cpu_warning_emitted = False
|
1184
|
-
self._users_dispatcher:
|
1179
|
+
self._users_dispatcher: UsersDispatcher | None = None
|
1185
1180
|
self.client = rpc.Client(master_host, master_port, self.client_id)
|
1186
1181
|
self.greenlet.spawn(self.worker).link_exception(greenlet_exception_handler)
|
1187
1182
|
self.connect_to_master()
|
@@ -1204,14 +1199,14 @@ class WorkerRunner(DistributedRunner):
|
|
1204
1199
|
self.environment.events.spawning_complete.add_listener(on_spawning_complete)
|
1205
1200
|
|
1206
1201
|
# register listener that adds the current number of spawned users to the report that is sent to the master node
|
1207
|
-
def on_report_to_master(client_id: str, data:
|
1202
|
+
def on_report_to_master(client_id: str, data: dict[str, Any]):
|
1208
1203
|
data["user_classes_count"] = self.user_classes_count
|
1209
1204
|
data["user_count"] = self.user_count
|
1210
1205
|
|
1211
1206
|
self.environment.events.report_to_master.add_listener(on_report_to_master)
|
1212
1207
|
|
1213
1208
|
# register listener that sends quit message to master
|
1214
|
-
def on_quitting(environment:
|
1209
|
+
def on_quitting(environment: Environment, **kw) -> None:
|
1215
1210
|
self.client.send(Message("quit", None, self.client_id))
|
1216
1211
|
|
1217
1212
|
self.environment.events.quitting.add_listener(on_quitting)
|
@@ -1224,11 +1219,11 @@ class WorkerRunner(DistributedRunner):
|
|
1224
1219
|
self.environment.events.user_error.add_listener(on_user_error)
|
1225
1220
|
|
1226
1221
|
def start(
|
1227
|
-
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes:
|
1222
|
+
self, user_count: int, spawn_rate: float, wait: bool = False, user_classes: list[type[User]] | None = None
|
1228
1223
|
) -> None:
|
1229
1224
|
raise NotImplementedError("use start_worker")
|
1230
1225
|
|
1231
|
-
def start_worker(self, user_classes_count:
|
1226
|
+
def start_worker(self, user_classes_count: dict[str, int], **kwargs) -> None:
|
1232
1227
|
"""
|
1233
1228
|
Start running a load test as a worker
|
1234
1229
|
|
@@ -1241,8 +1236,8 @@ class WorkerRunner(DistributedRunner):
|
|
1241
1236
|
if self.environment.host:
|
1242
1237
|
user_class.host = self.environment.host
|
1243
1238
|
|
1244
|
-
user_classes_spawn_count:
|
1245
|
-
user_classes_stop_count:
|
1239
|
+
user_classes_spawn_count: dict[str, int] = {}
|
1240
|
+
user_classes_stop_count: dict[str, int] = {}
|
1246
1241
|
|
1247
1242
|
for user_class_name, user_class_count in user_classes_count.items():
|
1248
1243
|
if self.user_classes_count[user_class_name] > user_class_count:
|
@@ -1378,9 +1373,7 @@ class WorkerRunner(DistributedRunner):
|
|
1378
1373
|
logger.error(f"Temporary connection lost to master server: {e}, will retry later.")
|
1379
1374
|
gevent.sleep(WORKER_REPORT_INTERVAL)
|
1380
1375
|
|
1381
|
-
def send_message(
|
1382
|
-
self, msg_type: str, data: Optional[Dict[str, Any]] = None, client_id: Optional[str] = None
|
1383
|
-
) -> None:
|
1376
|
+
def send_message(self, msg_type: str, data: dict[str, Any] | None = None, client_id: str | None = None) -> None:
|
1384
1377
|
"""
|
1385
1378
|
Sends a message to master node
|
1386
1379
|
|
@@ -1392,7 +1385,7 @@ class WorkerRunner(DistributedRunner):
|
|
1392
1385
|
self.client.send(Message(msg_type, data, self.client_id))
|
1393
1386
|
|
1394
1387
|
def _send_stats(self) -> None:
|
1395
|
-
data:
|
1388
|
+
data: dict[str, Any] = {}
|
1396
1389
|
self.environment.events.report_to_master.fire(client_id=self.client_id, data=data)
|
1397
1390
|
self.client.send(Message("stats", data, self.client_id))
|
1398
1391
|
|
@@ -1419,14 +1412,14 @@ class WorkerRunner(DistributedRunner):
|
|
1419
1412
|
self.connected = True
|
1420
1413
|
|
1421
1414
|
|
1422
|
-
def _format_user_classes_count_for_log(user_classes_count:
|
1415
|
+
def _format_user_classes_count_for_log(user_classes_count: dict[str, int]) -> str:
|
1423
1416
|
return "{} ({} total users)".format(
|
1424
1417
|
json.dumps(dict(sorted(user_classes_count.items(), key=itemgetter(0)))),
|
1425
1418
|
sum(user_classes_count.values()),
|
1426
1419
|
)
|
1427
1420
|
|
1428
1421
|
|
1429
|
-
def _aggregate_dispatched_users(d:
|
1422
|
+
def _aggregate_dispatched_users(d: dict[str, dict[str, int]]) -> dict[str, int]:
|
1430
1423
|
# TODO: Test it
|
1431
1424
|
user_classes = list(next(iter(d.values())).keys())
|
1432
1425
|
return {u: sum(d[u] for d in d.values()) for u in user_classes}
|
locust/shape.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
2
3
|
import time
|
3
|
-
from typing import ClassVar, Optional, Tuple, List, Type
|
4
4
|
from abc import ABCMeta, abstractmethod
|
5
|
+
from typing import TYPE_CHECKING, ClassVar
|
5
6
|
|
6
|
-
from . import User
|
7
7
|
from .runners import Runner
|
8
8
|
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from . import User
|
11
|
+
|
9
12
|
|
10
13
|
class LoadTestShapeMeta(ABCMeta):
|
11
14
|
"""
|
@@ -23,7 +26,7 @@ class LoadTestShape(metaclass=LoadTestShapeMeta):
|
|
23
26
|
Base class for custom load shapes.
|
24
27
|
"""
|
25
28
|
|
26
|
-
runner:
|
29
|
+
runner: Runner | None = None
|
27
30
|
"""Reference to the :class:`Runner <locust.runners.Runner>` instance"""
|
28
31
|
|
29
32
|
abstract: ClassVar[bool] = True
|
@@ -52,7 +55,7 @@ class LoadTestShape(metaclass=LoadTestShapeMeta):
|
|
52
55
|
return self.runner.user_count
|
53
56
|
|
54
57
|
@abstractmethod
|
55
|
-
def tick(self) ->
|
58
|
+
def tick(self) -> tuple[int, float] | tuple[int, float, list[type[User]] | None] | None:
|
56
59
|
"""
|
57
60
|
Returns a tuple with 2 elements to control the running load test:
|
58
61
|
|