-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding ability to generate any custom load shape with LoadTestShape class #1505
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1505 +/- ##
==========================================
+ Coverage 81.45% 81.50% +0.05%
==========================================
Files 27 28 +1
Lines 2410 2460 +50
Branches 372 380 +8
==========================================
+ Hits 1963 2005 +42
- Misses 356 359 +3
- Partials 91 96 +5
Continue to review full report at Codecov.
|
for _, msg in server.outbox: | ||
if msg.data: | ||
num_users += msg.data["num_users"] | ||
self.assertEqual(3, num_users, "Total number of users in second stage of shape test is not 3: %i" % num_users) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also test ramp down?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. I'll add another test. I had a problem that the test only runs for 5 seconds and is then stopped, regardless of what I set. I couldn't see why. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK now there are 2 tests, one for scaling up and one for scaling down. Ideally they would be in the same test but that 5s issue makes it too tight.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... I didnt know there was a 5 second limit and cant really understand where it comes from :)
locust/runners.py
Outdated
@@ -71,10 +73,13 @@ def on_request_failure(request_type, name, response_time, response_length, excep | |||
def on_hatch_complete(user_count): | |||
self.state = STATE_RUNNING | |||
if environment.reset_stats: | |||
logger.info("Resetting stats\n") | |||
logger.info("Resetting stats") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this change intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I thought the \n
was a mistake. It's intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll revert it anyway
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tbh I dont know :)
locust/runners.py
Outdated
|
||
|
||
if self.environment.shape_class: | ||
logger.debug("Starting with shape class") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you are kind of logging this twice? (here and on line 286)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I'll remove this one.
duration -- When this many seconds pass the test is advanced to the next stage | ||
users -- Total user count | ||
hatch_rate -- Hatch rate | ||
stop -- A boolean that can stop that test at a specific stage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a None entry could be better used to signal the end of the test? Because you'll never need to do (x, y, True) for any other values of x or y than zero, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well we need to return at least (Int, Int)
to edit the running test so you are saying to stop the test return (None, None)
? Or (Int, Int, None)
? Or something else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant that tick() should do return None
instead of return (0, 0, True)
(which of course requires modifications in runners.py on line 297 as wel)
But, I also think that stop_at_end is an unnecessary complication. If you want to run your test forever, cant you just set the last step to use 'duration': 9999999
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But, I also think that stop_at_end is an unnecessary complication
This is just an example of what I imagine someone might want to use or see before they write their own. But I can remove stop_at_end
it to make it simpler, no worries.
cant you just set the last step to use 'duration': 9999999
I would say no, that's not very precise. What if you forget it's running and it gets to 10000000 seconds😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to reiterate, these are examples. I imagine they would be in the docs or just here for people to find. They aren't to be copy/pasted and used as they are. I just imagined them to help a beginner to see how this class could be used 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Lets not spend too much time on them. On the other hand, if they are just examples then it makes sense to keep them very simplified (and let our users worry about edge cases like how to make it run forever etc).
locust/shape.py
Outdated
""" | ||
Calculates run time in seconds of the load test | ||
""" | ||
return round(time.monotonic() - self.start_time) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure a rounded time is a good thing to expose. This can give people the illusion that tick is called every second (which cannot be guaranteed because of gevent - the only thing your code guarantees is that it will not be called with less than one second between calls)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
examples/custom_shape/double_wave.py
Outdated
Keyword arguments: | ||
|
||
min_users -- minimum users | ||
peak_one -- first peak size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
peak_one sounds to me like "the number of users at the first peak", which could be misleading.
Not sure what would be a better name though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just threw this together to show how you could generate something totally custom and not related to "stages" or "step-load". I'll rework the wording here and maybe improve the maths.
examples/custom_shape/double_wave.py
Outdated
amplitude -- size of the test | ||
time_limit -- total length of test | ||
""" | ||
def __init__(self, min_users=20, peak_one=1, peak_two=1.5, amplitude=150, time_limit=600): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks a little weird to me.
It's not as if this constructor can be called from somewhere where these parameters are ever set, right?
Probably all these variables should be class level and then you can get rid of the parameters for the constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I'll fix.
examples/custom_shape/stages.py
Outdated
{'duration': 120, 'users': 70, 'hatch_rate': 10}, | ||
{'duration': 180, 'users': 100, 'hatch_rate': 10}, | ||
{'duration': 220, 'users': 10, 'hatch_rate': 10}, | ||
{'duration': 230, 'users': 5, 'hatch_rate': 10}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include a stage with ramp down just to show that it can be done, I think that would be nice!
(but maybe reduce the number of stages, it doesnt add much information)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does ramp down, see users
key, it goes up and then down 🙂
But OK, I'll reduce number of stages here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, my comment was about having so many stages, not about the very last stage :)
@cyberw I've addressed all your comments |
locust/shape.py
Outdated
""" | ||
run_time = self.get_run_time() | ||
|
||
return (0, 0, True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cyberw comments about what to return should go here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant that tick() should do return None instead of return (0, 0, True)
Is that consistent? Wouldn't something like (Int, Int)
, (None, None)
or ()
be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont know what you mean by (Int, Int) - how would that be used indicate the end of the test?
I think None is the most consistent of the options, also it requires the least amount of code and thought from the user.
It is not a ramp down to null users using ramping down at a rate of null per second, so your second option doesnt make sense to me.
I guess we could do "anything that is "falsy" (to allow ()
, None
and False
), if that sounds better to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant trying to keep the type that is returned consistent in all cases. It returns a tuple (of 2 integers normally (Int, Int)
) so if you we want to be consistent then it should always return a tuple, perhaps sometimes an empty one, ()
.
But I know this is a contentious topic in Python, especially for people coming from other languages who find it a bit too loose.
Ultimately I don't care enough to write more comments about this so I'll just change it to return (Int, Int)
or None
😃
@cyberw please re-review and when you're happy I'll work on docs |
locust/runners.py
Outdated
@@ -293,13 +293,13 @@ def shape_worker(self): | |||
logger.info("Shape worker starting") | |||
while self.state == STATE_INIT or self.state == STATE_HATCHING or self.state == STATE_RUNNING: | |||
new_state = self.environment.shape_class.tick() | |||
user_count, hatch_rate, stop_test = new_state | |||
if stop_test: | |||
if new_state == None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use is None
(in this particular case, I'm not sure it makes a functional difference, but it is still nicer :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you wish ✅
Looks great now. One question. How does it work when running web UI? @heyman I'll probably merge this soon. Last chance to give your view. Step two could be to remove the step-load thingy if you agree this is better. |
I still need to write docs and this is not a quick task IMO.
Like I said in my opening comment, basically if a load test is started via the UI or API, the hatch rate and users is simply ignored. I think that's fair, if you're using a custom shape you will be a pretty advanced user anyway. I was thinking we could log a warning? What do you think? |
I think logging a warning is fine (we should change that for -u/-r/-t parameters to make them behave that way as well tbh, but that may be a later thing) |
OK I've included a few words in the log message to indicate the user_count/hatch_rate are ignored. I've also disabled editing a running shape test as this doesn't make sense. I'll work on docs now. |
OK I wrote a nice doc. Personally I think this feature deserves a mention in Anything else @cyberw? |
Do I have to generate the API docs myself? Or is that done in CI or something?
|
While you are adding the warning about ignoring -u/... when running with web ui, can you do it for non-custom-load-shapes as well? I've been annoyed by that many times + it is good to be consistent. Bonus points if you prefill the web ui form with the values from parameters instead of warning :) Api docs are generated in CI (but if you install the readthedocs-sphinx-search package then you should be able to build them locally as well) |
Regarding the docs, I think you can add it the same way as we did step load (a mention in the menu and possibly also a link in a suitable place, but keep it in a separate rst file). And then after a while, we remove step load :P Or maybe re-implement it using this method. |
There's not really a place for me to log warning if user/hatch is passed because these are ALWAYS passed. In master, locust will throw an exception or error if you don't set user/hatch when starting a test.
I'm happy to add something but I'm not 100% sure what you mean. You mean a combination of command arguments and the UI? Or? Here's what I have now in this PR, when running locust from command line:
If you start a load test from the UI or API and there is a shape class in use then user/hatch is ignored and this is logged:
Editing a running load test with a shape class nothing but will log:
|
If you create an new issue (it sounds separate) then I'll happily address it in a new PR 🙂 |
Looking really good now. Is it worth adding a test case for master/worker setup as well? (I’m on mobile so if there already is one then maybe i missed it :) |
OK @cyberw, done, with master + 3 workers scaling up, down and stop. Are we ready? 😃 🙏 |
Looks amazing. One last question: what will happen if additional workers connect during the test? Will new users launch on the newly connected workers? Tbh I'm not sure that is a use case we need to provide full support for, but some people argued differently (when we were discussing global rps wait time I think). If it should work then maybe add it to the test? (adding a worker and then ramping up, making sure the users become balanced after ramp up) |
This behaviour is unchanged by this PR. Users are rebalanced across all workers when new workers connect. Does a test of that functionality have a relation to this PR? There is already the |
I was just thinking those two features might be worth testing together. Maybe not. Merging now! Really cool stuff! |
@cyberw when is a new release likely to be made? |
Tonight if you’re lucky, tomorrow if you are not :) |
You weren't lucky :( Also, I think I'll want to hold of for some more merges (1468 in particular seems to be very close) |
Also, can you make a PR that greys out the numbers that cant be edited in the UI when running with a shape? Also, I noticed something weird. What is going on here: Line 34 in 62fa37b
the variable |
I can.
That is intentional. The user has to override |
👍 I think it makes sense to remove it, feels like it adds about as much confusion as it removes :) Also, pylance complains about it. |
OK: #1524 |
Resolves #1432
Adding a base
LoadTestShape
class for users to inherit in order to generate anyuser_count
/hatch
at 1 second resolution.LoadTestShape
class and used it if presentgreenlet
on the master and will continuously edit/update and optionally stop a running load testLoadTestShape
class is present. This is stated in a log message.LoadTestShape
is present and a conflicting option is also specified as argument (users, hatch-rate or step-load)Tests:
LoadTestShape
class is not inuser_classes
TestMasterRunner
Initial testing looks good 🚀
Comments from @cyberw
Please give me suggestions if we need more tests as I'm not sure. This addition is simply editing a running test so much of this functionality should be covered already.
It will exit with an error.
The current behaviour here won't change?
Comments from @heyman
We can replace the stepload code with the more general approach in this PR but also this PR is quite similar to the stepload implementation. Please clarify.