locust 2.26.1.dev48__py3-none-any.whl → 2.26.1.dev61__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/_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.26.1.dev48'
16
- __version_tuple__ = version_tuple = (2, 26, 1, 'dev48')
15
+ __version__ = version = '2.26.1.dev61'
16
+ __version_tuple__ = version_tuple = (2, 26, 1, 'dev61')
locust/dispatch.py CHANGED
@@ -6,11 +6,12 @@ import math
6
6
  import time
7
7
  from collections import defaultdict
8
8
  from collections.abc import Generator, Iterator
9
+ from heapq import heapify, heapreplace
10
+ from math import log2
9
11
  from operator import attrgetter
10
12
  from typing import TYPE_CHECKING
11
13
 
12
14
  import gevent
13
- from roundrobin import smooth
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from locust import User
@@ -26,6 +27,33 @@ if TYPE_CHECKING:
26
27
  # profile = line_profiler.LineProfiler()
27
28
 
28
29
 
30
+ def _kl_generator(users: list[tuple[type[User], float]]) -> Iterator[str | None]:
31
+ """Generator based on Kullback-Leibler divergence
32
+
33
+ For example, given users A, B with weights 5 and 1 respectively,
34
+ this algorithm will yield AAABAAAAABAA.
35
+ """
36
+ if not users:
37
+ while True:
38
+ yield None
39
+
40
+ names = [u[0].__name__ for u in users]
41
+ weights = [u[1] for u in users]
42
+ generated = weights.copy()
43
+
44
+ heap = [(x * log2(x / (x + 1.0)), i) for i, x in enumerate(generated)]
45
+ heapify(heap)
46
+
47
+ while True:
48
+ i = heap[0][1] # choose element which choosing minimizes divergence the most
49
+ yield names[i]
50
+ generated[i] += 1.0
51
+ x = generated[i]
52
+ kl_diff = weights[i] * log2(x / (x + 1.0))
53
+ # calculate how much choosing element i for (x + 1)th time decreases divergence
54
+ heapreplace(heap, (kl_diff, i))
55
+
56
+
29
57
  class UsersDispatcher(Iterator):
30
58
  """
31
59
  Iterator that dispatches the users to the workers.
@@ -319,9 +347,7 @@ class UsersDispatcher(Iterator):
319
347
 
320
348
  def _distribute_users(
321
349
  self, target_user_count: int
322
- ) -> tuple[
323
- dict[str, dict[str, int]], Generator[str | None, None, None], itertools.cycle, list[tuple[WorkerNode, str]]
324
- ]:
350
+ ) -> tuple[dict[str, dict[str, int]], Iterator[str | None], itertools.cycle, list[tuple[WorkerNode, str]]]:
325
351
  """
326
352
  This function might take some time to complete if the `target_user_count` is a big number. A big number
327
353
  is typically > 50 000. However, this function is only called if a worker is added or removed while a test
@@ -350,71 +376,11 @@ class UsersDispatcher(Iterator):
350
376
 
351
377
  return users_on_workers, user_gen, worker_gen, active_users
352
378
 
353
- def _user_gen(self) -> Generator[str | None, None, None]:
354
- """
355
- This method generates users according to their weights using
356
- a smooth weighted round-robin algorithm implemented by https://github.com/linnik/roundrobin.
357
-
358
- For example, given users A, B with weights 5 and 1 respectively, this algorithm
359
- will yield AAABAAAAABAA. The smooth aspect of this algorithm is what makes it possible
360
- to keep the distribution during ramp-up and ramp-down. If we were to use a normal
361
- weighted round-robin algorithm, we'd get AAAAABAAAAAB which would make the distribution
362
- less accurate during ramp-up/down.
363
- """
364
-
365
- def infinite_cycle_gen(users: list[tuple[type[User], int]]) -> itertools.cycle:
366
- if not users:
367
- return itertools.cycle([None])
368
-
369
- def _get_order_of_magnitude(n: float) -> int:
370
- """Get how many times we need to multiply `n` to get an integer-like number.
371
- For example:
372
- 0.1 would return 10,
373
- 0.04 would return 100,
374
- 0.0007 would return 10000.
375
- """
376
- if n <= 0:
377
- raise ValueError("To get the order of magnitude, the number must be greater than 0.")
378
-
379
- counter = 0
380
- while n < 1:
381
- n *= 10
382
- counter += 1
383
- return 10**counter
384
-
385
- # Get maximum order of magnitude to "normalize the weights".
386
- # "Normalizing the weights" is to multiply all weights by the same number so that
387
- # they become integers. Then we can find the largest common divisor of all the
388
- # weights, divide them by it and get the smallest possible numbers with the same
389
- # ratio as the numbers originally had.
390
- max_order_of_magnitude = _get_order_of_magnitude(min(abs(u[1]) for u in users))
391
- weights = tuple(int(u[1] * max_order_of_magnitude) for u in users)
392
-
393
- greatest_common_divisor = math.gcd(*weights)
394
- normalized_values = [
395
- (
396
- user[0].__name__,
397
- normalized_weight // greatest_common_divisor,
398
- )
399
- for user, normalized_weight in zip(users, weights)
400
- ]
401
- generation_length_to_get_proper_distribution = sum(
402
- normalized_val[1] for normalized_val in normalized_values
403
- )
404
- gen = smooth(normalized_values)
405
-
406
- # Instead of calling `gen()` for each user, we cycle through a generator of fixed-length
407
- # `generation_length_to_get_proper_distribution`. Doing so greatly improves performance because
408
- # we only ever need to call `gen()` a relatively small number of times. The length of this generator
409
- # is chosen as the sum of the normalized weights. So, for users A, B, C of weights 2, 5, 6, the length is
410
- # 2 + 5 + 6 = 13 which would yield the distribution `CBACBCBCBCABC` that gets repeated over and over
411
- # until the target user count is reached.
412
- return itertools.cycle(gen() for _ in range(generation_length_to_get_proper_distribution))
413
-
379
+ def _user_gen(self) -> Iterator[str | None]:
414
380
  fixed_users = {u.__name__: u for u in self._user_classes if u.fixed_count}
415
381
 
416
- cycle_fixed_gen = infinite_cycle_gen([(u, u.fixed_count) for u in fixed_users.values()])
417
- cycle_weighted_gen = infinite_cycle_gen([(u, u.weight) for u in self._user_classes if not u.fixed_count])
382
+ fixed_users_gen = _kl_generator([(u, u.fixed_count) for u in fixed_users.values()])
383
+ weighted_users_gen = _kl_generator([(u, u.weight) for u in self._user_classes if not u.fixed_count])
418
384
 
419
385
  # Spawn users
420
386
  while True:
@@ -423,7 +389,7 @@ class UsersDispatcher(Iterator):
423
389
  current_fixed_users_count = {u: self._get_user_current_count(u) for u in fixed_users}
424
390
  spawned_classes: set[str] = set()
425
391
  while len(spawned_classes) != len(fixed_users):
426
- user_name: str | None = next(cycle_fixed_gen)
392
+ user_name: str | None = next(fixed_users_gen)
427
393
  if not user_name:
428
394
  break
429
395
 
@@ -439,7 +405,7 @@ class UsersDispatcher(Iterator):
439
405
  else:
440
406
  spawned_classes.add(user_name)
441
407
 
442
- yield next(cycle_weighted_gen)
408
+ yield next(weighted_users_gen)
443
409
 
444
410
  @staticmethod
445
411
  def _fast_users_on_workers_copy(users_on_workers: dict[str, dict[str, int]]) -> dict[str, dict[str, int]]:
@@ -5,6 +5,7 @@ from locust.dispatch import UsersDispatcher
5
5
  from locust.runners import WorkerNode
6
6
  from locust.test.util import clear_all_functools_lru_cache
7
7
 
8
+ import math
8
9
  import time
9
10
  import unittest
10
11
  from operator import attrgetter
@@ -3924,7 +3925,6 @@ class TestRampUpDifferentUsers(unittest.TestCase):
3924
3925
 
3925
3926
  user_dispatcher.new_dispatch(target_user_count=21, spawn_rate=21, user_classes=[User1, User2, User3])
3926
3927
  dispatched_users = next(user_dispatcher)
3927
- print(dispatched_users)
3928
3928
  self.assertDictEqual(
3929
3929
  dispatched_users,
3930
3930
  {
@@ -4123,6 +4123,40 @@ class TestRampUpDifferentUsers(unittest.TestCase):
4123
4123
  self.assertEqual(_user_count_on_worker(dispatched_users, worker_nodes[2].id), 6)
4124
4124
 
4125
4125
 
4126
+ class TestFloatWeithts(unittest.TestCase):
4127
+ def test_float_weights(self):
4128
+ """Final distribution should be {"User1": 3, "User2": 3, "User3": 3}"""
4129
+
4130
+ for ratio in (1, 1.0, 10, 2.5, 0.3, 1 / 23, math.e, math.pi):
4131
+
4132
+ class User1(User):
4133
+ weight = 1 * ratio
4134
+
4135
+ class User2(User):
4136
+ weight = 2 * ratio
4137
+
4138
+ class User3(User):
4139
+ weight = 3 * ratio
4140
+
4141
+ worker_node1 = WorkerNode("1")
4142
+ worker_node2 = WorkerNode("2")
4143
+ worker_node3 = WorkerNode("3")
4144
+
4145
+ sleep_time = 0 # Speed-up test
4146
+
4147
+ users_dispatcher = UsersDispatcher(
4148
+ worker_nodes=[worker_node1, worker_node2, worker_node3], user_classes=[User1, User2, User3]
4149
+ )
4150
+ users_dispatcher.new_dispatch(target_user_count=9, spawn_rate=0.5)
4151
+ users_dispatcher._wait_between_dispatch = sleep_time
4152
+
4153
+ if ratio == 1:
4154
+ reference = list(users_dispatcher)
4155
+ else:
4156
+ for x in reference:
4157
+ self.assertDictEqual(x, next(users_dispatcher))
4158
+
4159
+
4126
4160
  def _aggregate_dispatched_users(d: dict[str, dict[str, int]]) -> dict[str, int]:
4127
4161
  user_classes = list(next(iter(d.values())).keys())
4128
4162
  return {u: sum(d[u] for d in d.values()) for u in user_classes}
locust/user/users.py CHANGED
@@ -1,21 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
- import time
5
- import traceback
6
- from typing import Callable, final
7
-
8
- from gevent import GreenletExit, greenlet
9
- from gevent.pool import Group
10
- from urllib3 import PoolManager
11
-
12
- logger = logging.getLogger(__name__)
13
3
  from locust.clients import HttpSession
14
4
  from locust.exception import LocustError, StopUser
15
- from locust.user.wait_time import constant
16
- from locust.util import deprecation
17
-
18
- from .task import (
5
+ from locust.user.task import (
19
6
  LOCUST_STATE_RUNNING,
20
7
  LOCUST_STATE_STOPPING,
21
8
  LOCUST_STATE_WAITING,
@@ -23,6 +10,17 @@ from .task import (
23
10
  TaskSet,
24
11
  get_tasks_from_base_classes,
25
12
  )
13
+ from locust.user.wait_time import constant
14
+ from locust.util import deprecation
15
+
16
+ import logging
17
+ import time
18
+ import traceback
19
+ from typing import Callable, final
20
+
21
+ from gevent import GreenletExit, greenlet
22
+ from gevent.pool import Group
23
+ from urllib3 import PoolManager
26
24
 
27
25
  logger = logging.getLogger(__name__)
28
26
 
@@ -105,27 +103,27 @@ class User(metaclass=UserMeta):
105
103
  tasks = {ThreadPage:15, write_post:1}
106
104
  """
107
105
 
108
- weight = 1
106
+ weight: float = 1
109
107
  """Probability of user class being chosen. The higher the weight, the greater the chance of it being chosen."""
110
108
 
111
- fixed_count = 0
109
+ fixed_count: int = 0
112
110
  """
113
111
  If the value > 0, the weight property will be ignored and the 'fixed_count'-instances will be spawned.
114
112
  These Users are spawned first. If the total target count (specified by the --users arg) is not enough
115
113
  to spawn all instances of each User class with the defined property, the final count of each User is undefined.
116
114
  """
117
115
 
118
- abstract = True
116
+ abstract: bool = True
119
117
  """If abstract is True, the class is meant to be subclassed, and locust will not spawn users of this class during a test."""
120
118
 
121
- def __init__(self, environment):
119
+ def __init__(self, environment) -> None:
122
120
  super().__init__()
123
121
  self.environment = environment
124
122
  """A reference to the :py:class:`Environment <locust.env.Environment>` in which this user is running"""
125
- self._state = None
126
- self._greenlet: greenlet.Greenlet = None
123
+ self._state: str | None = None
124
+ self._greenlet: greenlet.Greenlet | None = None
127
125
  self._group: Group
128
- self._taskset_instance: TaskSet = None
126
+ self._taskset_instance: TaskSet | None = None
129
127
  self._cp_last_run = time.time() # used by constant_pacing wait_time
130
128
 
131
129
  def on_start(self) -> None:
@@ -191,7 +189,7 @@ class User(metaclass=UserMeta):
191
189
  self._group = group
192
190
  return self._greenlet
193
191
 
194
- def stop(self, force=False):
192
+ def stop(self, force: bool = False):
195
193
  """
196
194
  Stop the user greenlet.
197
195
 
@@ -251,7 +249,7 @@ class HttpUser(User):
251
249
  for keeping a user session between requests.
252
250
  """
253
251
 
254
- abstract = True
252
+ abstract: bool = True
255
253
  """If abstract is True, the class is meant to be subclassed, and users will not choose this locust during a test"""
256
254
 
257
255
  pool_manager: PoolManager | None = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: locust
3
- Version: 2.26.1.dev48
3
+ Version: 2.26.1.dev61
4
4
  Summary: Developer friendly load testing framework
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/locustio/locust
@@ -37,7 +37,6 @@ Requires-Dist: ConfigArgParse >=1.5.5
37
37
  Requires-Dist: psutil >=5.9.1
38
38
  Requires-Dist: Flask-Login >=0.6.3
39
39
  Requires-Dist: Flask-Cors >=3.0.10
40
- Requires-Dist: roundrobin >=0.0.2
41
40
  Requires-Dist: pywin32 ; platform_system == "Windows"
42
41
  Requires-Dist: tomli >=1.1.0 ; python_version < "3.11"
43
42
 
@@ -1,10 +1,10 @@
1
1
  locust/__init__.py,sha256=g6oA-Ba_hs3gLWVf5MKJ1mvfltI8MFnDWG8qslqm8yg,1402
2
2
  locust/__main__.py,sha256=vBQ82334kX06ImDbFlPFgiBRiLIinwNk3z8Khs6hd74,31
3
- locust/_version.py,sha256=gadSK3eqNa51B7ixflfmjSwqkQoR97QFFuc15b40Crw,428
3
+ locust/_version.py,sha256=LEYNsmYxTmpPGy-8xPWBC-TOuPW8IR_1QcZb3AZjrPE,428
4
4
  locust/argument_parser.py,sha256=izMXLuMZWUpS6m8SrGRmOjLfPPuYWXCvQFicRmn-a90,28774
5
5
  locust/clients.py,sha256=YKuAyMAbxs8_-w7XJw0hc67KFBNNLxibsw6FwiS01Q8,14781
6
6
  locust/debug.py,sha256=We6Z9W0btkKSc7PxWmrZx-xMynvOOsKhG6jmDgQin0g,5134
7
- locust/dispatch.py,sha256=S2pAMOlbadOrtMTLTDkq1Pvqes3HVUdZl-K5SDss6ig,19313
7
+ locust/dispatch.py,sha256=vYh0QEDFgJ3hY0HgSk-EiNO7IP9ffzXF_Et8wB9JvsI,16995
8
8
  locust/env.py,sha256=nd6ui1bv6n-kkLkP3r61ZkskDY627dsKOAkYHhtOW7o,12472
9
9
  locust/event.py,sha256=xgNKbcejxy1TNUfIdgV75KgD2_BOwQmvjrJ4hWuydRw,7740
10
10
  locust/exception.py,sha256=jGgJ32ubuf4pWdlaVOkbh2Y0LlG0_DHi-lv3ib8ppOE,1791
@@ -53,7 +53,7 @@ locust/test/fake_module2_for_env_test.py,sha256=dzGYWCr1SSkd8Yyo68paUNrCNW7YY_Qg
53
53
  locust/test/mock_locustfile.py,sha256=4xgoAYlhvdIBjGsLFFN0abpTNM7k12iSkrfTPUQhAMQ,1271
54
54
  locust/test/mock_logging.py,sha256=qapKrKhTdlVc8foJB2Hxjn7SB6soaLeAj3VF4A6kZtw,806
55
55
  locust/test/test_debugging.py,sha256=omQ0w5_Xh1xuTBzkd3VavEIircwtlmoOEHcMInY67vU,1053
56
- locust/test/test_dispatch.py,sha256=CIO10mC0FL8FjubV0jNZfd3q8EFQdZhLlm4QnN7HbPs,167754
56
+ locust/test/test_dispatch.py,sha256=RjoncanN4FFt-aiTl4G8XRoc81n6fwfO8CacbjzpvP8,168856
57
57
  locust/test/test_env.py,sha256=l0fLl9nubdgzxwFNajmBkJvQc5cO5rOTE4p12lbCbs0,8919
58
58
  locust/test/test_fasthttp.py,sha256=jVA5wWjZxXYW6emzy-lfPC0AOabzT6rDCX0N7DPP9mc,30727
59
59
  locust/test/test_http.py,sha256=VQCVY0inLC0RS-V3E9WHL3vBLGokZjQt0zKSrTNlQmM,12536
@@ -80,7 +80,7 @@ locust/user/__init__.py,sha256=S2yvmI_AU9kXirtTIVqiV_Hs7yXzqXvaSgkNo9ig-fk,71
80
80
  locust/user/inspectuser.py,sha256=KgrWHyE5jhK6or58R7soLRf-_st42AaQrR72qbiXw9E,2641
81
81
  locust/user/sequential_taskset.py,sha256=E8yykSZBO-QMcza1frr-7l8Cv_5bbSpjRO6sbkmGpZE,2544
82
82
  locust/user/task.py,sha256=JvVVCQ1_UQSsahqaEZoFCD-cBXlOJLJ51ewXHNesSAI,16700
83
- locust/user/users.py,sha256=qhOW5dDmGbsukWDVb1YDs92D_vbCKRIW60jB5I2bRxs,9950
83
+ locust/user/users.py,sha256=BsKyxzLq1lmdYBXnwHNyMH_Nxfy7QRlyLnfiE8539HY,9989
84
84
  locust/user/wait_time.py,sha256=bGRKMVx4lom75sX3POYJUa1CPeME2bEAXG6CEgxSO5U,2675
85
85
  locust/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
86
86
  locust/util/cache.py,sha256=IxbpGawl0-hoWKvCrtksxjSLf2GbBBTVns06F7mFBkM,1062
@@ -95,9 +95,9 @@ locust/webui/dist/report.html,sha256=sOdZZVgZbqgu86BBCSQf3uQUYXgmgSnXF32JpnyAII8
95
95
  locust/webui/dist/assets/favicon.ico,sha256=IUl-rYqfpHdV38e-s0bkmFIeLS-n3Ug0DQxk-h202hI,8348
96
96
  locust/webui/dist/assets/index-941b6e82.js,sha256=G3n5R81Svt0HzbWaV3AV20jLWGLr4X50UZ-Adu2KcxU,1645614
97
97
  locust/webui/dist/assets/logo.png,sha256=EIVPqr6wE_yqguHaqFHIsH0ZACLSrvNWyYO7PbyIj4w,19299
98
- locust-2.26.1.dev48.dist-info/LICENSE,sha256=78XGpIn3fHVBfaxlPNUfjVufSN7QsdhpJMRJHv2AFpo,1095
99
- locust-2.26.1.dev48.dist-info/METADATA,sha256=9VQCOppNg6gNaf0Np6zUnh-1qPSGR-2gm52m8ewpeJM,7301
100
- locust-2.26.1.dev48.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
101
- locust-2.26.1.dev48.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
102
- locust-2.26.1.dev48.dist-info/top_level.txt,sha256=XSsjgPA8Ggf9TqKVbkwSqZFuPlZ085X13M9orDycE20,7
103
- locust-2.26.1.dev48.dist-info/RECORD,,
98
+ locust-2.26.1.dev61.dist-info/LICENSE,sha256=78XGpIn3fHVBfaxlPNUfjVufSN7QsdhpJMRJHv2AFpo,1095
99
+ locust-2.26.1.dev61.dist-info/METADATA,sha256=iKcIMdLVlX5wXgc8qsYQMxWjMh5C0tLwzuSRnPSXsxA,7267
100
+ locust-2.26.1.dev61.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
101
+ locust-2.26.1.dev61.dist-info/entry_points.txt,sha256=RAdt8Ku-56m7bFjmdj-MBhbF6h4NX7tVODR9QNnOg0E,44
102
+ locust-2.26.1.dev61.dist-info/top_level.txt,sha256=XSsjgPA8Ggf9TqKVbkwSqZFuPlZ085X13M9orDycE20,7
103
+ locust-2.26.1.dev61.dist-info/RECORD,,