Skip to content
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

Work towards 1.0. Refactoring of runners/events/web ui. Getting rid of global state. #1266

Merged
merged 47 commits into from
Mar 10, 2020
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
78dba23
WIP: Refactor internal code to get rid of global LocustRunner instanc…
heyman Feb 23, 2020
2dd2087
Remove LocustRunner.hatch_rate (the start and spawn methods takes hat…
heyman Feb 23, 2020
021d928
Add Environment.host (move from options.host in order to not have to …
heyman Feb 23, 2020
81e12b0
Add Environment.reset_stats (move from options.reset_stats in order t…
heyman Feb 23, 2020
aae6bb1
stats_printer only needs access to RequestStats instance, and not the…
heyman Feb 23, 2020
32216d8
Merge branch 'master' into v1.0
heyman Feb 23, 2020
f7e4317
Log actual exception in retry exception handler
heyman Feb 25, 2020
4cdb808
Set strict_map_key=False when unserializing master/slave messages, si…
heyman Feb 25, 2020
48d94fc
Always round response times, stored in response_times dict, to integer.
heyman Feb 25, 2020
40a159f
Merge branch 'master' into v1.0
heyman Feb 25, 2020
ee30daa
Move LocustRunner.step_load into Environment.step_load
heyman Feb 26, 2020
3923cef
Fix broken CSV stats writer
heyman Feb 26, 2020
4ae8b2b
Speed up test
heyman Feb 26, 2020
46ad14c
Remove EventHook.__iadd__ and EventHook.__isub__ methods in favour of…
heyman Feb 27, 2020
ac79808
Make add_listener return handler, because it’ll make it possible to u…
heyman Feb 27, 2020
705fcae
Merge branch 'master' into v1.0
heyman Feb 27, 2020
1a7c3a4
Remove unused import
heyman Feb 27, 2020
21d4833
Fix bug where EventHook.fire(reverse=True) would permanently reverse …
heyman Feb 27, 2020
5a7704c
Add locust.event.init event hook that can be used by end-users to run…
heyman Feb 27, 2020
91019fa
Fix re-structured text syntax
heyman Feb 27, 2020
9c559f7
Fix example in docstring
heyman Feb 27, 2020
c12f615
Update extending-locust and API documentation page for the new API
heyman Feb 27, 2020
c4fcd0e
Remove deprecated info about installing libev on macOS
heyman Feb 27, 2020
fbc04ce
Fix example code that used old API for specifying wait time in millis…
heyman Feb 27, 2020
33a068b
Rewrote the “Common libraries” section in the documentation, and rena…
heyman Feb 27, 2020
489987f
Add documentation page about running Locust as a lib (so far it only …
heyman Feb 27, 2020
5f6937c
Update documentation on custom clients to new event API.
heyman Feb 27, 2020
23f2388
Update example to use new event API
heyman Feb 27, 2020
9e1a816
Remove LocustRunner.request_stats property
heyman Feb 28, 2020
8444d09
Remove HttpSession’s dependency on the Environment instance, and inst…
heyman Feb 28, 2020
507d546
More refactoring. Move stats and locust_clases from Environment onto …
heyman Feb 28, 2020
3f8d700
Add LocustRunner and WebUI instances as argument to the init event
heyman Mar 2, 2020
9fdeb12
Reinstate code for saving and restoring event listeners, since we sti…
heyman Mar 3, 2020
84153b0
Improve code for creating temporary locustfiles that can be used in t…
heyman Mar 3, 2020
d1f7c4e
Refactor code for parsing command line arguments.
heyman Mar 3, 2020
fd1ce43
Remove global locust.events.events Event instance, in favour of using…
heyman Mar 3, 2020
8a1f4b5
Update example (event arguments was changed)
heyman Mar 3, 2020
f6715f5
Rename locust.events module to locust.event
heyman Mar 3, 2020
cfba346
Add missing change from previous commit
heyman Mar 3, 2020
d5ec17e
Replace --heartbeat-liveness and --heartbeat-interval command line op…
heyman Mar 4, 2020
1cc9450
Add more config variables to Environment class (get rid of LocustRunn…
heyman Mar 4, 2020
b380197
Make sure init event is fired before the runner is started.
heyman Mar 5, 2020
b945a80
Do set process exit code to non-zero when CPU warning has been emitte…
heyman Mar 5, 2020
0758a8f
Remove master_host/master_port/master_bind_host/master_bind_port from…
heyman Mar 6, 2020
216f771
When stopping a test, log a warning message if the CPU went above 90%…
heyman Mar 6, 2020
3f9e0ad
Escape failure messages
heyman Mar 10, 2020
4115010
Merge branch 'master' into v1.0
heyman Mar 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,25 +77,42 @@ InterruptTaskSet Exception
.. autoexception:: locust.exception.InterruptTaskSet


Environment class
=================
.. autoclass:: locust.env.Environment
:members:


.. _events:

Event hooks
===========

The event hooks are instances of the **locust.events.EventHook** class:
Locust provide event hooks that can be used to extend Locus in various ways

.. autoclass:: locust.events.EventHook
Events directly under locust.events
-----------------------------------

.. note::
.. automodule:: locust.events
:members: init

It's highly recommended that you add a wildcard keyword argument in your event listeners
to prevent your code from breaking if new arguments are added in a future version.
Events under Environment.events:
--------------------------------

The following event hooks are available under :py:attr:`Environment.events <locust.env.Environment.events>`:

Available hooks
.. autoclass:: locust.events.Events
:members:


EventHook class
---------------

The following event hooks are available under the **locust.events** module:
The event hooks are instances of the **locust.events.EventHook** class:

.. automodule:: locust.events
:members: request_success, request_failure, locust_error, report_to_master, slave_report, hatch_complete, quitting
.. autoclass:: locust.events.EventHook

.. note::

It's highly recommended that you add a wildcard keyword argument in your event listeners
to prevent your code from breaking if new arguments are added in a future version.
44 changes: 32 additions & 12 deletions docs/extending-locust.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,33 @@
Extending Locust
=================

Locust comes with a number of events that provides hooks for extending locust in different ways.
Locust comes with a number of events hooks that can be used to extend Locust in different ways.

Event listeners can be registered at the module level in a locust file. Here's an example::
Most event hooks live on the Environment instance under the :py:attr:`events <locust.env.Environment.events>`
attribute. However the Environment instance is not available at the module level of your test
file, so in order to utilize the :py:attr:`events <locust.env.Environment.events>` on the
:py:class:`Environment <locust.env.Environment>` class, you can utilize the :py:attr:`locust.events.init`
event which is triggered when Locust is started.

from locust import events
Adding the following code at the module level of you locustfile.py will print a message when Locust starts::

def my_success_handler(request_type, name, response_time, response_length, **kw):
print "Successfully fetched: %s" % (name)
from locust.events import init

@init.add_listener
def on_locust_init(environment, **kw):
print("Locust is starting")


We can use this event listener to setup listeners for other events::

from locust.events import init

@init.add_listener
def on_locust_init(environment, **kw):
@environment.events.request_success
def my_success_handler(request_type, name, response_time, response_length, **kw):
print("Successfully made a request to: %s" % name)

events.request_success += my_success_handler

.. note::

Expand All @@ -29,12 +46,15 @@ Adding Web Routes
==================

Locust uses Flask to serve the web UI and therefore it is easy to add web end-points to the web UI.
Just import the Flask app in your locustfile and set up a new route::
From the Environment instance, we can access the Flask app and set up a new route::

from locust import web

@web.app.route("/added_page")
def my_added_page():
return "Another page"
from locust.events import init

@init.add_listener
def on_locust_init(environment, **kw):
@environment.web_ui.app.route("/added_page")
def my_added_page():
return "Another page"

You should now be able to start locust and browse to http://127.0.0.1:8089/added_page

1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Other functionalities
testing-other-systems
extending-locust
logging
use-as-lib


API
Expand Down
12 changes: 3 additions & 9 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,10 @@ Once you've done that you should be able to just ``pip install locustio``.
Installing Locust on macOS
--------------------------

The following is currently the shortest path to installing gevent on OS X using Homebrew.
Make sure you have a working installation of Python 3.6 or higher and follow the above
instructions. `Homebrew <http://mxcl.github.com/homebrew/>`_ can be used to install Python
on macOS.

#. Install `Homebrew <http://mxcl.github.com/homebrew/>`_.
#. Install libev (dependency for gevent):

.. code-block:: console

brew install libev

#. Then follow the above instructions.

Increasing Maximum Number of Open Files Limit
---------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ For example, for an exponentially distributed wait time with average of 1 second

class WebsiteUser(HttpLocust):
task_set = UserBehaviour
wait_time = lambda self: random.expovariate(1)*1000
wait_time = lambda self: random.expovariate(1)


Start Locust
Expand Down
6 changes: 3 additions & 3 deletions docs/running-locust-in-step-load-mode.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ Options
=======

``--step-load``
------------
----------------

Enable Step Load mode to monitor how performance metrics varies when user load increases.


``--step-clients``
-----------
-------------------

Client count to increase by step in Step Load mode. Only used together with ``--step-load``.

Expand All @@ -33,7 +33,7 @@ Step duration in Step Load mode, e.g. (300s, 20m, 3h, 1h30m, etc.). Only used to


Running Locust in step load mode without the web UI
---------------------------------
----------------------------------------------------

If you want to run Locust in step load mode without the web UI, you can do that with ``--step-clients`` and ``--step-time``:

Expand Down
9 changes: 5 additions & 4 deletions docs/testing-other-systems.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ Testing other systems using custom clients

Locust was built with HTTP as its main target. However, it can easily be extended to load test
any request/response based system, by writing a custom client that triggers
:py:attr:`request_success <locust.events.request_success>` and
:py:attr:`request_failure <locust.events.request_failure>` events.
:py:attr:`request_success <locust.events.Events.request_success>` and
:py:attr:`request_failure <locust.events.Events.request_failure>` events.

Sample XML-RPC Locust client
============================
Expand All @@ -19,10 +19,11 @@ If you've written Locust tests before, you'll recognize the class called *ApiUse
Locust class that has a *TaskSet* class with *tasks* in its *task_set* attribute. However, the *ApiUser*
inherits from *XmlRpcLocust* that you can see right above ApiUser. The *XmlRpcLocust* class provides an
instance of XmlRpcClient under the *client* attribute. The *XmlRpcClient* is a wrapper around the standard
library's :py:class:`xmlrpclib.ServerProxy`. It basically just proxies the function calls, but with the
important addition of firing :py:attr:`locust.events.request_success` and :py:attr:`locust.events.request_failure`
library's :py:class:`xmlrpc.client.ServerProxy`. It basically just proxies the function calls, but with the
important addition of firing :py:attr:`locust.events.Events.request_success` and :py:attr:`locust.events.Events.request_failure`
events, which will make all calls reported in Locust's statistics.

Here's an implementation of an XML-RPC server that would work as a server for the code above:

.. literalinclude:: ../examples/custom_xmlrpc_client/server.py

46 changes: 46 additions & 0 deletions docs/use-as-lib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
==========================
Using Locust as a library
==========================

It's possible to use Locust as a library instead of running Locust by invoking the ``locust`` command.

Here's an example::

import gevent
from locust import HttpLocust, TaskSet, task, between
from locust.runners import LocalLocustRunner
from locust.env import Environment
from locust.stats import stats_printer
from locust.log import setup_logging
from locust.web import WebUI

setup_logging("INFO", None)


class User(HttpLocust):
wait_time = between(1, 3)
host = "https://docs.locust.io"

class task_set(TaskSet):
@task
def my_task(self):
self.client.get("/")

@task
def task_404(self):
self.client.get("/non-existing-path")

# setup Environment and Runner
env = Environment(locust_classes=[User])
runner = LocalLocustRunner(environment=env)
# start a WebUI instance
web_ui = WebUI(runner=runner, environment=env)
gevent.spawn(lambda: web_ui.start("127.0.0.1", 8089))

# start a greenlet that periodically outputs the current stats
gevent.spawn(stats_printer(env.stats))

# start the test
runner.start(1, hatch_rate=10)
# wait for the greenlets (indefinitely)
runner.greenlet.join()
65 changes: 27 additions & 38 deletions docs/writing-a-locustfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,61 +473,50 @@ Example:
for i in range(10):
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")

Common libraries
=================

Often, people wish to group multiple locustfiles that share common libraries. In that case, it is important
to define the *project root* to be the directory where you invoke locust, and it is suggested that all
locustfiles live somewhere beneath the project root.
How to structure your test code
================================

A flat file structure works out of the box:
It's important to remember that the locustfile.py is just an ordinary Python module that is imported
by Locust. From this module you're free to import other python code just as you normally would
in any Python program. The current working directory is automatically added to python's ``sys.path``,
so any python file/module/packages that resides in the working directory can be imported using the
python ``import`` statement.

* project root
For small tests, keeping all of the test code in a single ``locustfile.py`` should work fine, but for
larger test suites, you'll probably want to split the code into multiple files and directories.

* ``commonlib_config.py``
How you structure the test source code is ofcourse entirely up to you, but we recommend that you
follow Python best practices. Here's an example file structure of an imaginary Locust project:

* ``commonlib_auth.py``

* ``locustfile_web_app.py``

* ``locustfile_api.py``

* ``locustfile_ecommerce.py``

The locustfiles may import common libraries using, e.g. ``import commonlib_auth``. This approach does not
cleanly separate common libraries from locust files, however.

Subdirectories can be a cleaner approach (see example below), but locust will only import modules relative to
the directory in which the running locustfile is placed. If you wish to import from your project root (i.e. the
location where you are running the locust command), make sure to write ``sys.path.append(os.getcwd())`` in your
locust file(s) before importing any common libraries---this will make the project root (i.e. the current
working directory) importable.

* project root

* ``__init__.py``
* Project root

* ``common/``

* ``__init__.py``

* ``auth.py``
* ``config.py``
* ``locustfile.py``
* ``requirements.txt`` (External Python dependencies is often kept in a requirements.txt)

* ``auth.py``
A project with multiple different locustfiles could also keep them in a separate subdirectory:

* ``locustfiles/``
* Project root

* ``common/``

* ``__init__.py``

* ``web_app.py``

* ``auth.py``
* ``config.py``
* ``locustfiles/``

* ``api.py``
* ``website.py``
* ``requirements.txt``

* ``ecommerce.py``

With the above project structure, your locust files can import common libraries using:
With any ofthe above project structure, your locustfile can import common libraries using:

.. code-block:: python

sys.path.append(os.getcwd())
import common.auth
2 changes: 1 addition & 1 deletion examples/custom_xmlrpc_client/server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import random
import time
from SimpleXMLRPCServer import SimpleXMLRPCServer
from xmlrpc.server import SimpleXMLRPCServer


def get_time():
Expand Down
16 changes: 10 additions & 6 deletions examples/custom_xmlrpc_client/xmlrpc_locustfile.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import time
import xmlrpclib
from xmlrpc.client import ServerProxy, Fault

from locust import Locust, TaskSet, events, task, between


class XmlRpcClient(xmlrpclib.ServerProxy):
class XmlRpcClient(ServerProxy):
"""
Simple, sample XML RPC client implementation that wraps xmlrpclib.ServerProxy and
fires locust events on request_success and request_failure, so that all requests
gets tracked in locust's statistics.
"""

_locust_environment = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just call this _environment? Not that it is super important...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just wanted to make sure that we wouldn't clash with anything in xmlrpclib.ServerProxy.


def __getattr__(self, name):
func = xmlrpclib.ServerProxy.__getattr__(self, name)
func = ServerProxy.__getattr__(self, name)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
except xmlrpclib.Fault as e:
except Fault as e:
total_time = int((time.time() - start_time) * 1000)
events.request_failure.fire(request_type="xmlrpc", name=name, response_time=total_time, exception=e)
self._locust_environment.events.request_failure.fire(request_type="xmlrpc", name=name, response_time=total_time, exception=e)
else:
total_time = int((time.time() - start_time) * 1000)
events.request_success.fire(request_type="xmlrpc", name=name, response_time=total_time, response_length=0)
self._locust_environment.events.request_success.fire(request_type="xmlrpc", name=name, response_time=total_time, response_length=0)
# In this example, I've hardcoded response_length=0. If we would want the response length to be
# reported correctly in the statistics, we would probably need to hook in at a lower level

Expand All @@ -36,6 +39,7 @@ class XmlRpcLocust(Locust):
def __init__(self, *args, **kwargs):
super(XmlRpcLocust, self).__init__(*args, **kwargs)
self.client = XmlRpcClient(self.host)
self.client._locust_environment = self.environment


class ApiUser(XmlRpcLocust):
Expand Down
Loading