locust 2.20.1.dev28__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.dev28.dist-info → locust-2.20.2.dist-info}/METADATA +2 -2
- locust-2.20.2.dist-info/RECORD +105 -0
- locust-2.20.1.dev28.dist-info/RECORD +0 -102
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/LICENSE +0 -0
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/WHEEL +0 -0
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/entry_points.txt +0 -0
- {locust-2.20.1.dev28.dist-info → locust-2.20.2.dist-info}/top_level.txt +0 -0
locust/__init__.py
CHANGED
@@ -15,15 +15,15 @@ from gevent import monkey
|
|
15
15
|
monkey.patch_all()
|
16
16
|
|
17
17
|
from ._version import version as __version__
|
18
|
-
from .user.sequential_taskset import SequentialTaskSet
|
19
|
-
from .user import wait_time
|
20
|
-
from .user.task import task, tag, TaskSet
|
21
|
-
from .user.users import HttpUser, User
|
22
18
|
from .contrib.fasthttp import FastHttpUser
|
23
|
-
from .user.wait_time import between, constant, constant_pacing, constant_throughput
|
24
|
-
from .shape import LoadTestShape
|
25
19
|
from .debug import run_single_user
|
26
20
|
from .event import Events
|
21
|
+
from .shape import LoadTestShape
|
22
|
+
from .user import wait_time
|
23
|
+
from .user.sequential_taskset import SequentialTaskSet
|
24
|
+
from .user.task import TaskSet, tag, task
|
25
|
+
from .user.users import HttpUser, User
|
26
|
+
from .user.wait_time import between, constant, constant_pacing, constant_throughput
|
27
27
|
|
28
28
|
events = Events()
|
29
29
|
|
@@ -46,5 +46,5 @@ __all__ = (
|
|
46
46
|
)
|
47
47
|
|
48
48
|
# Used for raising a DeprecationWarning if old Locust/HttpLocust is used
|
49
|
-
from .util.deprecation import DeprecatedLocustClass as Locust
|
50
49
|
from .util.deprecation import DeprecatedHttpLocustClass as HttpLocust
|
50
|
+
from .util.deprecation import DeprecatedLocustClass as Locust
|
locust/_version.py
CHANGED
@@ -12,5 +12,5 @@ __version__: str
|
|
12
12
|
__version_tuple__: VERSION_TUPLE
|
13
13
|
version_tuple: VERSION_TUPLE
|
14
14
|
|
15
|
-
__version__ = version = '2.20.
|
16
|
-
__version_tuple__ = version_tuple = (2, 20,
|
15
|
+
__version__ = version = '2.20.2'
|
16
|
+
__version_tuple__ = version_tuple = (2, 20, 2)
|
locust/argument_parser.py
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import locust
|
4
|
+
|
1
5
|
import os
|
2
6
|
import platform
|
3
7
|
import sys
|
4
8
|
import textwrap
|
5
|
-
from typing import
|
6
|
-
import configargparse
|
9
|
+
from typing import Any, NamedTuple
|
7
10
|
|
8
|
-
import
|
11
|
+
import configargparse
|
9
12
|
|
10
13
|
version = locust.__version__
|
11
14
|
|
@@ -38,11 +41,11 @@ class LocustArgumentParser(configargparse.ArgumentParser):
|
|
38
41
|
return action
|
39
42
|
|
40
43
|
@property
|
41
|
-
def args_included_in_web_ui(self) ->
|
44
|
+
def args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
|
42
45
|
return {a.dest: a for a in self._actions if hasattr(a, "include_in_web_ui") and a.include_in_web_ui}
|
43
46
|
|
44
47
|
@property
|
45
|
-
def secret_args_included_in_web_ui(self) ->
|
48
|
+
def secret_args_included_in_web_ui(self) -> dict[str, configargparse.Action]:
|
46
49
|
return {
|
47
50
|
a.dest: a
|
48
51
|
for a in self._actions
|
@@ -91,7 +94,7 @@ def find_locustfile(locustfile):
|
|
91
94
|
# Implicit 'return None' if nothing was found
|
92
95
|
|
93
96
|
|
94
|
-
def find_locustfiles(locustfiles:
|
97
|
+
def find_locustfiles(locustfiles: list[str], is_directory: bool) -> list[str]:
|
95
98
|
"""
|
96
99
|
Returns a list of relative file paths for the Locustfile Picker. If is_directory is True,
|
97
100
|
locustfiles is expected to have a single index which is a directory that will be searched for
|
@@ -176,7 +179,7 @@ See documentation for more details, including how to set options using a file or
|
|
176
179
|
return parser
|
177
180
|
|
178
181
|
|
179
|
-
def parse_locustfile_option(args=None) ->
|
182
|
+
def parse_locustfile_option(args=None) -> list[str]:
|
180
183
|
"""
|
181
184
|
Construct a command line parser that is only used to parse the -f argument so that we can
|
182
185
|
import the test scripts in case any of them adds additional command line arguments to the
|
@@ -304,6 +307,13 @@ def setup_parser_arguments(parser):
|
|
304
307
|
dest="list_commands",
|
305
308
|
help="Show list of possible User classes and exit",
|
306
309
|
)
|
310
|
+
parser.add_argument(
|
311
|
+
"--config-users",
|
312
|
+
type=str,
|
313
|
+
nargs="*",
|
314
|
+
help="User configuration as a JSON string or file. A list of arguments or an Array of JSON configuration may be provided",
|
315
|
+
env_var="LOCUST_CONFIG_USERS",
|
316
|
+
)
|
307
317
|
|
308
318
|
web_ui_group = parser.add_argument_group("Web UI options")
|
309
319
|
web_ui_group.add_argument(
|
@@ -355,9 +365,16 @@ def setup_parser_arguments(parser):
|
|
355
365
|
dest="web_auth",
|
356
366
|
metavar="<username:password>",
|
357
367
|
default=None,
|
358
|
-
help=
|
368
|
+
help=configargparse.SUPPRESS,
|
359
369
|
env_var="LOCUST_WEB_AUTH",
|
360
370
|
)
|
371
|
+
web_ui_group.add_argument(
|
372
|
+
"--web-login",
|
373
|
+
default=False,
|
374
|
+
action="store_true",
|
375
|
+
help="Protects the web interface with a login page. See https://docs.locust.io/en/stable/extending-locust.html#authentication",
|
376
|
+
env_var="LOCUST_WEB_LOGIN",
|
377
|
+
)
|
361
378
|
web_ui_group.add_argument(
|
362
379
|
"--tls-cert",
|
363
380
|
default="",
|
@@ -429,6 +446,13 @@ def setup_parser_arguments(parser):
|
|
429
446
|
help="How long should the master wait for workers to connect before giving up. Defaults to wait forever",
|
430
447
|
env_var="LOCUST_EXPECT_WORKERS_MAX_WAIT",
|
431
448
|
)
|
449
|
+
master_group.add_argument(
|
450
|
+
"--enable-rebalancing",
|
451
|
+
action="store_true",
|
452
|
+
default=False,
|
453
|
+
dest="enable_rebalancing",
|
454
|
+
help="Re-distribute users if new workers are added or removed during a test run. Experimental.",
|
455
|
+
)
|
432
456
|
master_group.add_argument(
|
433
457
|
"--expect-slaves",
|
434
458
|
action="store_true",
|
@@ -611,13 +635,6 @@ Typically ONLY these options (and --locustfile) need to be specified on workers,
|
|
611
635
|
dest="equal_weights",
|
612
636
|
help="Use equally distributed task weights, overriding the weights specified in the locustfile.",
|
613
637
|
)
|
614
|
-
other_group.add_argument(
|
615
|
-
"--enable-rebalancing",
|
616
|
-
action="store_true",
|
617
|
-
default=False,
|
618
|
-
dest="enable_rebalancing",
|
619
|
-
help="Allow to automatically rebalance users if new workers are added or removed during a test run.",
|
620
|
-
)
|
621
638
|
|
622
639
|
user_classes_group = parser.add_argument_group("User classes")
|
623
640
|
user_classes_group.add_argument(
|
@@ -660,10 +677,10 @@ class UIExtraArgOptions(NamedTuple):
|
|
660
677
|
default_value: str
|
661
678
|
is_secret: bool
|
662
679
|
help_text: str
|
663
|
-
choices:
|
680
|
+
choices: list[str] | None = None
|
664
681
|
|
665
682
|
|
666
|
-
def ui_extra_args_dict(args=None) ->
|
683
|
+
def ui_extra_args_dict(args=None) -> dict[str, dict[str, Any]]:
|
667
684
|
"""Get all the UI visible arguments"""
|
668
685
|
locust_args = default_args_dict()
|
669
686
|
|
@@ -684,7 +701,7 @@ def ui_extra_args_dict(args=None) -> Dict[str, Dict[str, Any]]:
|
|
684
701
|
return extra_args
|
685
702
|
|
686
703
|
|
687
|
-
def locustfile_is_directory(locustfiles:
|
704
|
+
def locustfile_is_directory(locustfiles: list[str]) -> bool:
|
688
705
|
"""
|
689
706
|
If a user passes in a locustfile without a file extension and there is a directory with the same name,
|
690
707
|
this function defaults to using the file and will raise a warning.
|
locust/clients.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
+
|
2
3
|
import re
|
3
4
|
import time
|
4
5
|
from contextlib import contextmanager
|
5
|
-
from typing import Generator
|
6
|
+
from typing import Generator
|
6
7
|
from urllib.parse import urlparse, urlunparse
|
7
8
|
|
8
9
|
import requests
|
@@ -48,7 +49,7 @@ class HttpSession(requests.Session):
|
|
48
49
|
and then mark it as successful even if the response code was not (i.e 500 or 404).
|
49
50
|
"""
|
50
51
|
|
51
|
-
def __init__(self, base_url, request_event, user, *args, pool_manager:
|
52
|
+
def __init__(self, base_url, request_event, user, *args, pool_manager: PoolManager | None = None, **kwargs):
|
52
53
|
super().__init__(*args, **kwargs)
|
53
54
|
|
54
55
|
self.base_url = base_url
|
@@ -57,7 +58,7 @@ class HttpSession(requests.Session):
|
|
57
58
|
|
58
59
|
# User can group name, or use the group context manager to gather performance statistics under a specific name
|
59
60
|
# This is an alternative to passing in the "name" parameter to the requests function
|
60
|
-
self.request_name:
|
61
|
+
self.request_name: str | None = None
|
61
62
|
|
62
63
|
# Check for basic authentication
|
63
64
|
parsed_url = urlparse(self.base_url)
|
@@ -301,7 +302,7 @@ class ResponseContextManager(LocustResponse):
|
|
301
302
|
|
302
303
|
|
303
304
|
class LocustHttpAdapter(HTTPAdapter):
|
304
|
-
def __init__(self, pool_manager:
|
305
|
+
def __init__(self, pool_manager: PoolManager | None, *args, **kwargs):
|
305
306
|
self.poolmanager = pool_manager
|
306
307
|
super().__init__(*args, **kwargs)
|
307
308
|
|
@@ -323,5 +324,5 @@ def _failure(self):
|
|
323
324
|
)
|
324
325
|
|
325
326
|
|
326
|
-
Response.success = _success
|
327
|
-
Response.failure = _failure
|
327
|
+
Response.success = _success # type: ignore[attr-defined]
|
328
|
+
Response.failure = _failure # type: ignore[attr-defined]
|
locust/contrib/fasthttp.py
CHANGED
@@ -1,35 +1,35 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
|
3
|
-
import
|
2
|
+
|
3
|
+
from locust.env import Environment
|
4
|
+
from locust.exception import CatchResponseError, LocustError, ResponseError
|
5
|
+
from locust.user import User
|
6
|
+
from locust.util.deprecation import DeprecatedFastHttpLocustClass as FastHttpLocust
|
7
|
+
|
4
8
|
import json
|
5
9
|
import json as unshadowed_json # some methods take a named parameter called json
|
10
|
+
import re
|
11
|
+
import socket
|
12
|
+
import time
|
13
|
+
import traceback
|
6
14
|
from base64 import b64encode
|
7
15
|
from contextlib import contextmanager
|
16
|
+
from http.cookiejar import CookieJar
|
8
17
|
from json.decoder import JSONDecodeError
|
9
|
-
from urllib.parse import urlparse, urlunparse
|
10
18
|
from ssl import SSLError
|
11
|
-
import
|
12
|
-
import
|
13
|
-
from typing import Callable, Optional, Tuple, Dict, Any, Generator, cast
|
14
|
-
|
15
|
-
from http.cookiejar import CookieJar
|
19
|
+
from typing import Any, Callable, Generator, cast
|
20
|
+
from urllib.parse import urlparse, urlunparse
|
16
21
|
|
17
22
|
import gevent
|
23
|
+
from charset_normalizer import detect
|
18
24
|
from gevent.timeout import Timeout
|
19
25
|
from geventhttpclient._parser import HTTPParseError
|
20
26
|
from geventhttpclient.client import HTTPClientPool
|
21
|
-
from geventhttpclient.useragent import UserAgent, CompatRequest, CompatResponse, ConnectionError
|
22
|
-
from geventhttpclient.response import HTTPConnectionClosed, HTTPSocketPoolResponse
|
23
27
|
from geventhttpclient.header import Headers
|
28
|
+
from geventhttpclient.response import HTTPConnectionClosed, HTTPSocketPoolResponse
|
29
|
+
from geventhttpclient.useragent import CompatRequest, CompatResponse, ConnectionError, UserAgent
|
24
30
|
|
25
31
|
# borrow requests's content-type header parsing
|
26
32
|
from requests.utils import get_encoding_from_headers
|
27
|
-
from charset_normalizer import detect
|
28
|
-
|
29
|
-
from locust.user import User
|
30
|
-
from locust.exception import LocustError, CatchResponseError, ResponseError
|
31
|
-
from locust.env import Environment
|
32
|
-
from locust.util.deprecation import DeprecatedFastHttpLocustClass as FastHttpLocust
|
33
33
|
|
34
34
|
# Monkey patch geventhttpclient.useragent.CompatRequest so that Cookiejar works with Python >= 3.3
|
35
35
|
# More info: https://github.com/requests/requests/pull/871
|
@@ -79,10 +79,10 @@ class FastHttpSession:
|
|
79
79
|
self,
|
80
80
|
environment: Environment,
|
81
81
|
base_url: str,
|
82
|
-
user:
|
82
|
+
user: User | None,
|
83
83
|
insecure=True,
|
84
|
-
client_pool:
|
85
|
-
ssl_context_factory:
|
84
|
+
client_pool: HTTPClientPool | None = None,
|
85
|
+
ssl_context_factory: Callable | None = None,
|
86
86
|
**kwargs,
|
87
87
|
):
|
88
88
|
self.environment = environment
|
@@ -313,7 +313,7 @@ class FastHttpUser(User):
|
|
313
313
|
insecure: bool = True
|
314
314
|
"""Parameter passed to FastHttpSession. Default True, meaning no SSL verification."""
|
315
315
|
|
316
|
-
default_headers:
|
316
|
+
default_headers: dict | None = None
|
317
317
|
"""Parameter passed to FastHttpSession. Adds the listed headers to every request."""
|
318
318
|
|
319
319
|
concurrency: int = 10
|
@@ -321,10 +321,10 @@ class FastHttpUser(User):
|
|
321
321
|
Note that setting this value has no effect when custom client_pool was given, and you need to spawn a your own gevent pool
|
322
322
|
to use it (as Users only have one greenlet). See test_fasthttp.py / test_client_pool_concurrency for an example."""
|
323
323
|
|
324
|
-
client_pool:
|
324
|
+
client_pool: HTTPClientPool | None = None
|
325
325
|
"""HTTP client pool to use. If not given, a new pool is created per single user."""
|
326
326
|
|
327
|
-
ssl_context_factory:
|
327
|
+
ssl_context_factory: Callable | None = None
|
328
328
|
"""A callable that return a SSLContext for overriding the default context created by the FastHttpSession."""
|
329
329
|
|
330
330
|
abstract = True
|
@@ -360,7 +360,7 @@ class FastHttpUser(User):
|
|
360
360
|
|
361
361
|
@contextmanager
|
362
362
|
def rest(
|
363
|
-
self, method, url, headers:
|
363
|
+
self, method, url, headers: dict | None = None, **kwargs
|
364
364
|
) -> Generator[RestResponseContextManager, None, None]:
|
365
365
|
"""
|
366
366
|
A wrapper for self.client.request that:
|
@@ -423,36 +423,36 @@ class FastHttpUser(User):
|
|
423
423
|
|
424
424
|
|
425
425
|
class FastRequest(CompatRequest):
|
426
|
-
payload:
|
426
|
+
payload: str | None = None
|
427
427
|
|
428
428
|
@property
|
429
|
-
def body(self) ->
|
429
|
+
def body(self) -> str | None:
|
430
430
|
return self.payload
|
431
431
|
|
432
432
|
|
433
433
|
class FastResponse(CompatResponse):
|
434
|
-
headers:
|
434
|
+
headers: Headers | None = None
|
435
435
|
"""Dict like object containing the response headers"""
|
436
436
|
|
437
|
-
_response:
|
437
|
+
_response: HTTPSocketPoolResponse | None = None
|
438
438
|
|
439
|
-
encoding:
|
439
|
+
encoding: str | None = None
|
440
440
|
"""In some cases setting the encoding explicitly is needed. If so, do it before calling .text"""
|
441
441
|
|
442
|
-
request:
|
442
|
+
request: FastRequest | None = None
|
443
443
|
|
444
444
|
def __init__(
|
445
445
|
self,
|
446
446
|
ghc_response: HTTPSocketPoolResponse,
|
447
|
-
request:
|
448
|
-
sent_request:
|
447
|
+
request: FastRequest | None = None,
|
448
|
+
sent_request: str | None = None,
|
449
449
|
):
|
450
450
|
super().__init__(ghc_response, request, sent_request)
|
451
451
|
|
452
452
|
self.request = request
|
453
453
|
|
454
454
|
@property
|
455
|
-
def text(self) ->
|
455
|
+
def text(self) -> str | None:
|
456
456
|
"""
|
457
457
|
Returns the text content of the response as a decoded string
|
458
458
|
"""
|
@@ -472,7 +472,7 @@ class FastResponse(CompatResponse):
|
|
472
472
|
return str(self.content, str(self.encoding), errors="replace")
|
473
473
|
|
474
474
|
@property
|
475
|
-
def url(self) ->
|
475
|
+
def url(self) -> str | None:
|
476
476
|
"""
|
477
477
|
Get "response" URL, which is the same as the request URL. This is a small deviation from HttpSession, which gets the final (possibly redirected) URL.
|
478
478
|
"""
|
@@ -500,6 +500,11 @@ class FastResponse(CompatResponse):
|
|
500
500
|
"""
|
501
501
|
return self._response.get_code() if self._response is not None else 0
|
502
502
|
|
503
|
+
@property
|
504
|
+
def ok(self):
|
505
|
+
"""Returns True if :attr:`status_code` is less than 400, False if not."""
|
506
|
+
return self.status_code < 400
|
507
|
+
|
503
508
|
def _content(self):
|
504
509
|
if self.headers is None:
|
505
510
|
return None
|
@@ -522,11 +527,11 @@ class ErrorResponse:
|
|
522
527
|
that doesn't have a real Response object attached. E.g. a socket error or similar
|
523
528
|
"""
|
524
529
|
|
525
|
-
headers:
|
530
|
+
headers: Headers | None = None
|
526
531
|
content = None
|
527
532
|
status_code = 0
|
528
|
-
error:
|
529
|
-
text:
|
533
|
+
error: Exception | None = None
|
534
|
+
text: str | None = None
|
530
535
|
request: CompatRequest
|
531
536
|
|
532
537
|
def __init__(self, url: str, request: CompatRequest):
|
@@ -542,7 +547,7 @@ class LocustUserAgent(UserAgent):
|
|
542
547
|
request_type = FastRequest
|
543
548
|
valid_response_codes = frozenset([200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 301, 302, 303, 304, 307])
|
544
549
|
|
545
|
-
def __init__(self, client_pool:
|
550
|
+
def __init__(self, client_pool: HTTPClientPool | None = None, **kwargs):
|
546
551
|
super().__init__(**kwargs)
|
547
552
|
|
548
553
|
if client_pool is not None:
|
locust/debug.py
CHANGED
@@ -1,13 +1,19 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
import inspect
|
1
|
+
from __future__ import annotations
|
2
|
+
|
4
3
|
import locust
|
5
4
|
import locust.log
|
6
|
-
from locust import
|
7
|
-
from typing import Type, Optional
|
5
|
+
from locust import argument_parser
|
8
6
|
from locust.env import Environment
|
9
7
|
from locust.exception import CatchResponseError, RescheduleTask
|
10
8
|
|
9
|
+
import inspect
|
10
|
+
import os
|
11
|
+
from datetime import datetime, timezone
|
12
|
+
from typing import TYPE_CHECKING
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from locust import User
|
16
|
+
|
11
17
|
|
12
18
|
def _print_t(s):
|
13
19
|
"""
|
@@ -94,16 +100,16 @@ class PrintListener:
|
|
94
100
|
print()
|
95
101
|
|
96
102
|
|
97
|
-
_env:
|
103
|
+
_env: Environment | None = None # minimal Environment for debugging
|
98
104
|
|
99
105
|
|
100
106
|
def run_single_user(
|
101
|
-
user_class:
|
107
|
+
user_class: type[User],
|
102
108
|
include_length=False,
|
103
109
|
include_time=False,
|
104
110
|
include_context=False,
|
105
111
|
include_payload=False,
|
106
|
-
loglevel:
|
112
|
+
loglevel: str | None = "WARNING",
|
107
113
|
):
|
108
114
|
"""
|
109
115
|
Runs a single User. Useful when you want to run a debugger.
|
locust/dispatch.py
CHANGED
@@ -1,19 +1,19 @@
|
|
1
|
-
from
|
1
|
+
from __future__ import annotations
|
2
|
+
|
2
3
|
import contextlib
|
3
4
|
import itertools
|
4
5
|
import math
|
5
6
|
import time
|
7
|
+
from collections import defaultdict
|
6
8
|
from collections.abc import Iterator
|
7
9
|
from operator import attrgetter
|
8
|
-
from typing import
|
10
|
+
from typing import TYPE_CHECKING, Generator
|
9
11
|
|
10
12
|
import gevent
|
11
|
-
|
12
13
|
from roundrobin import smooth
|
13
14
|
|
14
|
-
from locust import User
|
15
|
-
|
16
15
|
if TYPE_CHECKING:
|
16
|
+
from locust import User
|
17
17
|
from locust.runners import WorkerNode
|
18
18
|
|
19
19
|
|
@@ -49,7 +49,7 @@ class UsersDispatcher(Iterator):
|
|
49
49
|
from 10 to 100.
|
50
50
|
"""
|
51
51
|
|
52
|
-
def __init__(self, worker_nodes:
|
52
|
+
def __init__(self, worker_nodes: list[WorkerNode], user_classes: list[type[User]]):
|
53
53
|
"""
|
54
54
|
:param worker_nodes: List of worker nodes
|
55
55
|
:param user_classes: The user classes
|
@@ -79,16 +79,16 @@ class UsersDispatcher(Iterator):
|
|
79
79
|
|
80
80
|
self._current_user_count = self.get_current_user_count()
|
81
81
|
|
82
|
-
self._dispatcher_generator: Generator[
|
82
|
+
self._dispatcher_generator: Generator[dict[str, dict[str, int]], None, None] = None
|
83
83
|
|
84
84
|
self._user_generator = self._user_gen()
|
85
85
|
|
86
86
|
self._worker_node_generator = itertools.cycle(self._worker_nodes)
|
87
87
|
|
88
88
|
# To keep track of how long it takes for each dispatch iteration to compute
|
89
|
-
self._dispatch_iteration_durations:
|
89
|
+
self._dispatch_iteration_durations: list[float] = []
|
90
90
|
|
91
|
-
self._active_users:
|
91
|
+
self._active_users: list[tuple[WorkerNode, str]] = []
|
92
92
|
|
93
93
|
# TODO: Test that attribute is set when dispatching and unset when done dispatching
|
94
94
|
self._dispatch_in_progress = False
|
@@ -108,10 +108,10 @@ class UsersDispatcher(Iterator):
|
|
108
108
|
return self._dispatch_in_progress
|
109
109
|
|
110
110
|
@property
|
111
|
-
def dispatch_iteration_durations(self) ->
|
111
|
+
def dispatch_iteration_durations(self) -> list[float]:
|
112
112
|
return self._dispatch_iteration_durations
|
113
113
|
|
114
|
-
def __next__(self) ->
|
114
|
+
def __next__(self) -> dict[str, dict[str, int]]:
|
115
115
|
users_on_workers = next(self._dispatcher_generator)
|
116
116
|
# TODO: Is this necessary to copy the users_on_workers if we know
|
117
117
|
# it won't be mutated by external code?
|
@@ -122,7 +122,7 @@ class UsersDispatcher(Iterator):
|
|
122
122
|
worker_nodes_by_id = sorted(self._worker_nodes, key=lambda w: w.id)
|
123
123
|
|
124
124
|
# Give every worker an index indicating how many workers came before it on that host
|
125
|
-
workers_per_host = defaultdict(
|
125
|
+
workers_per_host = defaultdict(int)
|
126
126
|
for worker_node in worker_nodes_by_id:
|
127
127
|
host = worker_node.id.split("_")[0]
|
128
128
|
worker_node._index_within_host = workers_per_host[host]
|
@@ -131,7 +131,7 @@ class UsersDispatcher(Iterator):
|
|
131
131
|
# Sort again, first by index within host, to ensure Users get started evenly across hosts
|
132
132
|
self._worker_nodes = sorted(self._worker_nodes, key=lambda worker: (worker._index_within_host, worker.id))
|
133
133
|
|
134
|
-
def _dispatcher(self) -> Generator[
|
134
|
+
def _dispatcher(self) -> Generator[dict[str, dict[str, int]], None, None]:
|
135
135
|
self._dispatch_in_progress = True
|
136
136
|
|
137
137
|
if self._rebalance:
|
@@ -164,7 +164,7 @@ class UsersDispatcher(Iterator):
|
|
164
164
|
|
165
165
|
self._dispatch_in_progress = False
|
166
166
|
|
167
|
-
def new_dispatch(self, target_user_count: int, spawn_rate: float, user_classes:
|
167
|
+
def new_dispatch(self, target_user_count: int, spawn_rate: float, user_classes: list | None = None) -> None:
|
168
168
|
"""
|
169
169
|
Initialize a new dispatch cycle.
|
170
170
|
|
@@ -194,7 +194,7 @@ class UsersDispatcher(Iterator):
|
|
194
194
|
|
195
195
|
self._dispatch_iteration_durations.clear()
|
196
196
|
|
197
|
-
def add_worker(self, worker_node:
|
197
|
+
def add_worker(self, worker_node: WorkerNode) -> None:
|
198
198
|
"""
|
199
199
|
This method is to be called when a new worker connects to the master. When
|
200
200
|
a new worker is added, the users dispatcher will flag that a rebalance is required
|
@@ -207,7 +207,7 @@ class UsersDispatcher(Iterator):
|
|
207
207
|
self._sort_workers()
|
208
208
|
self._prepare_rebalance()
|
209
209
|
|
210
|
-
def remove_worker(self, worker_node:
|
210
|
+
def remove_worker(self, worker_node: WorkerNode) -> None:
|
211
211
|
"""
|
212
212
|
This method is similar to the above `add_worker`. When a worker disconnects
|
213
213
|
(because of e.g. network failure, worker failure, etc.), this method will ensure that the next
|
@@ -268,7 +268,7 @@ class UsersDispatcher(Iterator):
|
|
268
268
|
sleep_duration = max(0.0, self._wait_between_dispatch - delta)
|
269
269
|
gevent.sleep(sleep_duration)
|
270
270
|
|
271
|
-
def _add_users_on_workers(self) ->
|
271
|
+
def _add_users_on_workers(self) -> dict[str, dict[str, int]]:
|
272
272
|
"""Add users on the workers until the target number of users is reached for the current dispatch iteration
|
273
273
|
|
274
274
|
:return: The users that we want to run on the workers
|
@@ -290,7 +290,7 @@ class UsersDispatcher(Iterator):
|
|
290
290
|
|
291
291
|
return self._users_on_workers
|
292
292
|
|
293
|
-
def _remove_users_from_workers(self) ->
|
293
|
+
def _remove_users_from_workers(self) -> dict[str, dict[str, int]]:
|
294
294
|
"""Remove users from the workers until the target number of users is reached for the current dispatch iteration
|
295
295
|
|
296
296
|
:return: The users that we want to run on the workers
|
@@ -318,8 +318,8 @@ class UsersDispatcher(Iterator):
|
|
318
318
|
|
319
319
|
def _distribute_users(
|
320
320
|
self, target_user_count: int
|
321
|
-
) ->
|
322
|
-
|
321
|
+
) -> tuple[
|
322
|
+
dict[str, dict[str, int]], Generator[str | None, None, None], itertools.cycle, list[tuple[WorkerNode, str]]
|
323
323
|
]:
|
324
324
|
"""
|
325
325
|
This function might take some time to complete if the `target_user_count` is a big number. A big number
|
@@ -349,7 +349,7 @@ class UsersDispatcher(Iterator):
|
|
349
349
|
|
350
350
|
return users_on_workers, user_gen, worker_gen, active_users
|
351
351
|
|
352
|
-
def _user_gen(self) -> Generator[
|
352
|
+
def _user_gen(self) -> Generator[str | None, None, None]:
|
353
353
|
"""
|
354
354
|
This method generates users according to their weights using
|
355
355
|
a smooth weighted round-robin algorithm implemented by https://github.com/linnik/roundrobin.
|
@@ -361,7 +361,7 @@ class UsersDispatcher(Iterator):
|
|
361
361
|
less accurate during ramp-up/down.
|
362
362
|
"""
|
363
363
|
|
364
|
-
def infinite_cycle_gen(users:
|
364
|
+
def infinite_cycle_gen(users: list[tuple[type[User], int]]) -> itertools.cycle:
|
365
365
|
if not users:
|
366
366
|
return itertools.cycle([None])
|
367
367
|
|
@@ -401,9 +401,9 @@ class UsersDispatcher(Iterator):
|
|
401
401
|
if self._try_dispatch_fixed:
|
402
402
|
self._try_dispatch_fixed = False
|
403
403
|
current_fixed_users_count = {u: self._get_user_current_count(u) for u in fixed_users}
|
404
|
-
spawned_classes:
|
404
|
+
spawned_classes: set[str] = set()
|
405
405
|
while len(spawned_classes) != len(fixed_users):
|
406
|
-
user_name:
|
406
|
+
user_name: str | None = next(cycle_fixed_gen)
|
407
407
|
if not user_name:
|
408
408
|
break
|
409
409
|
|
@@ -422,7 +422,7 @@ class UsersDispatcher(Iterator):
|
|
422
422
|
yield next(cycle_weighted_gen)
|
423
423
|
|
424
424
|
@staticmethod
|
425
|
-
def _fast_users_on_workers_copy(users_on_workers:
|
425
|
+
def _fast_users_on_workers_copy(users_on_workers: dict[str, dict[str, int]]) -> dict[str, dict[str, int]]:
|
426
426
|
"""deepcopy is too slow, so we use this custom copy function.
|
427
427
|
|
428
428
|
The implementation was profiled and compared to other implementations such as dict-comprehensions
|